From df25ceffd4135622100201238c131fb2ee50ad42 Mon Sep 17 00:00:00 2001 From: "Marcel S. Henselin" Date: Wed, 17 Dec 2025 16:14:25 +0100 Subject: [PATCH] chore: cleanup alpha branch --- .../access_token/access_token_acc_test.go | 50 - .../access_token/ephemeral_resource.go | 132 - .../access_token/ephemeral_resource_test.go | 253 - .../testdata/ephemeral_resource.tf | 15 - .../testdata/service_account.json | 16 - stackit/internal/services/cdn/cdn_acc_test.go | 407 -- .../services/cdn/customdomain/datasource.go | 236 - .../cdn/customdomain/datasource_test.go | 137 - .../services/cdn/customdomain/resource.go | 534 -- .../cdn/customdomain/resource_test.go | 308 -- .../services/cdn/distribution/datasource.go | 214 - .../services/cdn/distribution/resource.go | 940 ---- .../cdn/distribution/resource_test.go | 589 -- stackit/internal/services/cdn/utils/util.go | 29 - .../internal/services/cdn/utils/util_test.go | 93 - stackit/internal/services/dns/dns_acc_test.go | 541 -- .../services/dns/recordset/datasource.go | 183 - .../services/dns/recordset/resource.go | 515 -- .../services/dns/recordset/resource_test.go | 375 -- .../services/dns/testdata/resource-max.tf | 74 - .../services/dns/testdata/resource-min.tf | 41 - stackit/internal/services/dns/utils/util.go | 29 - .../internal/services/dns/utils/util_test.go | 93 - .../internal/services/dns/zone/datasource.go | 268 - .../internal/services/dns/zone/resource.go | 603 -- .../services/dns/zone/resource_test.go | 437 -- stackit/internal/services/git/git_acc_test.go | 342 -- .../services/git/instance/datasource.go | 166 - .../services/git/instance/resource.go | 425 -- .../services/git/instance/resource_test.go | 225 - .../services/git/testdata/resource-max.tf | 14 - .../services/git/testdata/resource-min.tf | 8 - stackit/internal/services/git/utils/util.go | 29 - .../internal/services/git/utils/util_test.go | 93 - .../services/iaas/affinitygroup/const.go | 41 - .../services/iaas/affinitygroup/datasource.go | 165 - .../services/iaas/affinitygroup/resource.go | 388 -- .../iaas/affinitygroup/resource_test.go | 118 - .../internal/services/iaas/iaas_acc_test.go | 4842 ----------------- .../services/iaas/image/datasource.go | 361 -- .../services/iaas/image/datasource_test.go | 178 - .../internal/services/iaas/image/resource.go | 891 --- .../services/iaas/image/resource_test.go | 404 -- .../iaas/image/testdata/mock-image.txt | 1 - .../services/iaas/imagev2/datasource.go | 648 --- .../services/iaas/imagev2/datasource_test.go | 484 -- .../internal/services/iaas/keypair/const.go | 24 - .../services/iaas/keypair/datasource.go | 129 - .../services/iaas/keypair/resource.go | 387 -- .../services/iaas/keypair/resource_test.go | 211 - .../services/iaas/machinetype/datasource.go | 263 - .../iaas/machinetype/datasource_test.go | 263 - .../services/iaas/network/datasource.go | 402 -- .../services/iaas/network/datasource_test.go | 387 -- .../services/iaas/network/resource.go | 956 ---- .../services/iaas/network/resource_test.go | 818 --- .../services/iaas/networkarea/datasource.go | 242 - .../services/iaas/networkarea/resource.go | 1004 ---- .../iaas/networkarea/resource_test.go | 1123 ---- .../iaas/networkarearegion/datasource.go | 181 - .../iaas/networkarearegion/resource.go | 728 --- .../iaas/networkarearegion/resource_test.go | 1052 ---- .../iaas/networkarearoute/datasource.go | 186 - .../iaas/networkarearoute/resource.go | 739 --- .../iaas/networkarearoute/resource_test.go | 623 --- .../iaas/networkinterface/datasource.go | 193 - .../iaas/networkinterface/resource.go | 683 --- .../iaas/networkinterface/resource_test.go | 368 -- .../iaas/networkinterfaceattach/resource.go | 328 -- .../services/iaas/project/datasource.go | 219 - .../services/iaas/project/datasource_test.go | 120 - .../services/iaas/publicip/datasource.go | 158 - .../services/iaas/publicip/resource.go | 453 -- .../services/iaas/publicip/resource_test.go | 281 - .../iaas/publicipassociate/resource.go | 389 -- .../iaas/publicipassociate/resource_test.go | 140 - .../iaas/publicipranges/datasource.go | 220 - .../iaas/publicipranges/datasource_test.go | 115 - .../services/iaas/securitygroup/datasource.go | 158 - .../services/iaas/securitygroup/resource.go | 472 -- .../iaas/securitygroup/resource_test.go | 230 - .../iaas/securitygrouprule/datasource.go | 214 - .../iaas/securitygrouprule/planmodifier.go | 93 - .../iaas/securitygrouprule/resource.go | 804 --- .../iaas/securitygrouprule/resource_test.go | 322 -- .../internal/services/iaas/server/const.go | 176 - .../services/iaas/server/datasource.go | 325 -- .../services/iaas/server/datasource_test.go | 175 - .../internal/services/iaas/server/resource.go | 1108 ---- .../services/iaas/server/resource_test.go | 623 --- .../iaas/serviceaccountattach/resource.go | 323 -- .../testdata/datasource-image-v2-variants.tf | 62 - .../iaas/testdata/datasource-machinetype.tf | 18 - .../testdata/datasource-public-ip-ranges.tf | 1 - .../testdata/resource-affinity-group-min.tf | 9 - .../iaas/testdata/resource-image-max.tf | 48 - .../iaas/testdata/resource-image-min.tf | 11 - .../iaas/testdata/resource-key-pair-max.tf | 11 - .../iaas/testdata/resource-key-pair-min.tf | 7 - .../testdata/resource-network-area-max.tf | 49 - .../testdata/resource-network-area-min.tf | 8 - .../resource-network-area-region-max.tf | 33 - .../resource-network-area-region-min.tf | 23 - .../resource-network-interface-max.tf | 54 - .../resource-network-interface-min.tf | 16 - .../iaas/testdata/resource-network-max.tf | 85 - .../iaas/testdata/resource-network-min.tf | 7 - .../testdata/resource-security-group-max.tf | 72 - .../testdata/resource-security-group-min.tf | 15 - .../resource-server-max-server-attachments.tf | 11 - .../iaas/testdata/resource-server-max.tf | 82 - .../iaas/testdata/resource-server-min.tf | 30 - .../iaas/testdata/resource-volume-max.tf | 36 - .../iaas/testdata/resource-volume-min.tf | 18 - stackit/internal/services/iaas/utils/util.go | 52 - .../internal/services/iaas/utils/util_test.go | 178 - .../services/iaas/volume/datasource.go | 188 - .../internal/services/iaas/volume/resource.go | 672 --- .../services/iaas/volume/resource_test.go | 268 - .../services/iaas/volumeattach/resource.go | 325 -- .../services/iaasalpha/iaasalpha_acc_test.go | 831 --- .../routingtable/route/datasource.go | 124 - .../iaasalpha/routingtable/route/resource.go | 570 -- .../routingtable/route/resource_test.go | 452 -- .../routingtable/routes/datasource.go | 187 - .../routingtable/routes/datasource_test.go | 199 - .../iaasalpha/routingtable/shared/route.go | 213 - .../routingtable/shared/route_test.go | 310 -- .../iaasalpha/routingtable/shared/shared.go | 264 - .../routingtable/table/datasource.go | 160 - .../routingtable/table/datasource_test.go | 136 - .../iaasalpha/routingtable/table/resource.go | 526 -- .../routingtable/table/resource_test.go | 212 - .../routingtable/tables/datasource.go | 216 - .../routingtable/tables/datasource_test.go | 175 - .../testdata/resource-routingtable-max.tf | 19 - .../testdata/resource-routingtable-min.tf | 9 - .../resource-routingtable-route-max.tf | 31 - .../resource-routingtable-route-min.tf | 27 - .../internal/services/iaasalpha/utils/util.go | 29 - .../services/iaasalpha/utils/util_test.go | 93 - .../internal/services/kms/key/datasource.go | 191 - stackit/internal/services/kms/key/resource.go | 455 -- .../services/kms/key/resource_test.go | 216 - .../services/kms/keyring/datasource.go | 144 - .../internal/services/kms/keyring/resource.go | 361 -- .../services/kms/keyring/resource_test.go | 173 - stackit/internal/services/kms/kms_acc_test.go | 1035 ---- .../internal/services/kms/testdata/key-max.tf | 27 - .../internal/services/kms/testdata/key-min.tf | 21 - .../services/kms/testdata/keyring-max.tf | 10 - .../services/kms/testdata/keyring-min.tf | 8 - .../services/kms/testdata/wrapping-key-max.tf | 25 - .../services/kms/testdata/wrapping-key-min.tf | 21 - stackit/internal/services/kms/utils/util.go | 29 - .../services/kms/wrapping-key/datasource.go | 179 - .../services/kms/wrapping-key/resource.go | 467 -- .../kms/wrapping-key/resource_test.go | 244 - .../loadbalancer/loadbalancer/datasource.go | 430 -- .../loadbalancer/loadbalancer/resource.go | 1755 ------ .../loadbalancer/resource_test.go | 953 ---- .../loadbalancer/loadbalancer_acc_test.go | 466 -- .../observability-credential/resource.go | 383 -- .../observability-credential/resource_test.go | 163 - .../loadbalancer/testfiles/resource-max.tf | 188 - .../loadbalancer/testfiles/resource-min.tf | 98 - .../services/loadbalancer/utils/util.go | 30 - .../services/loadbalancer/utils/util_test.go | 93 - .../services/logme/credential/datasource.go | 169 - .../services/logme/credential/resource.go | 343 -- .../logme/credential/resource_test.go | 134 - .../services/logme/instance/datasource.go | 296 - .../services/logme/instance/resource.go | 928 ---- .../services/logme/instance/resource_test.go | 402 -- .../internal/services/logme/logme_acc_test.go | 489 -- .../services/logme/testdata/resource-max.tf | 78 - .../services/logme/testdata/resource-min.tf | 28 - stackit/internal/services/logme/utils/util.go | 31 - .../services/logme/utils/util_test.go | 94 - .../services/mariadb/credential/datasource.go | 177 - .../services/mariadb/credential/resource.go | 375 -- .../mariadb/credential/resource_test.go | 221 - .../services/mariadb/instance/datasource.go | 232 - .../services/mariadb/instance/resource.go | 760 --- .../mariadb/instance/resource_test.go | 339 -- .../services/mariadb/mariadb_acc_test.go | 476 -- .../mariadb/testfiles/resource-max.tf | 40 - .../mariadb/testfiles/resource-min.tf | 16 - .../internal/services/mariadb/utils/util.go | 31 - .../services/mariadb/utils/util_test.go | 94 - .../modelserving/modelserving_acc_test.go | 158 - .../modelserving/token/description.md | 20 - .../services/modelserving/token/resource.go | 615 --- .../modelserving/token/resource_test.go | 341 -- .../services/modelserving/utils/util.go | 29 - .../services/modelserving/utils/util_test.go | 93 - .../mongodbflex/instance/datasource.go | 263 - .../services/mongodbflex/instance/resource.go | 1081 ---- .../mongodbflex/instance/resource_test.go | 1093 ---- .../mongodbflex/mongodbflex_acc_test.go | 346 -- .../services/mongodbflex/user/datasource.go | 233 - .../mongodbflex/user/datasource_test.go | 146 - .../services/mongodbflex/user/resource.go | 573 -- .../mongodbflex/user/resource_test.go | 501 -- .../services/mongodbflex/utils/util.go | 30 - .../services/mongodbflex/utils/util_test.go | 93 - .../objectstorage/bucket/datasource.go | 159 - .../services/objectstorage/bucket/resource.go | 375 -- .../objectstorage/bucket/resource_test.go | 156 - .../objectstorage/credential/datasource.go | 226 - .../credential/datasource_test.go | 125 - .../objectstorage/credential/resource.go | 580 -- .../objectstorage/credential/resource_test.go | 450 -- .../credentialsgroup/datasource.go | 150 - .../credentialsgroup/resource.go | 411 -- .../credentialsgroup/resource_test.go | 317 -- .../objectstorage/objectstorage_acc_test.go | 316 -- .../objectstorage/testfiles/resource-min.tf | 26 - .../services/objectstorage/utils/util.go | 30 - .../services/objectstorage/utils/util_test.go | 93 - .../observability/alertgroup/datasource.go | 173 - .../observability/alertgroup/resource.go | 574 -- .../observability/alertgroup/resource_test.go | 366 -- .../observability/credential/resource.go | 262 - .../observability/credential/resource_test.go | 77 - .../observability/instance/datasource.go | 534 -- .../observability/instance/resource.go | 2714 --------- .../observability/instance/resource_test.go | 1636 ------ .../log-alertgroup/datasource.go | 173 - .../observability/log-alertgroup/resource.go | 574 -- .../log-alertgroup/resource_test.go | 366 -- .../observability/observability_acc_test.go | 1073 ---- .../observability/scrapeconfig/datasource.go | 229 - .../observability/scrapeconfig/resource.go | 865 --- .../scrapeconfig/resource_test.go | 504 -- .../observability/testdata/resource-max.tf | 234 - .../observability/testdata/resource-min.tf | 69 - .../services/observability/utils/util.go | 32 - .../services/observability/utils/util_test.go | 94 - .../opensearch/credential/datasource.go | 177 - .../opensearch/credential/resource.go | 372 -- .../opensearch/credential/resource_test.go | 221 - .../opensearch/instance/datasource.go | 265 - .../services/opensearch/instance/resource.go | 840 --- .../opensearch/instance/resource_test.go | 373 -- .../opensearch/opensearch_acc_test.go | 304 -- .../services/opensearch/utils/util.go | 31 - .../services/opensearch/utils/util_test.go | 94 - .../postgresflex/database/datasource.go | 171 - .../postgresflex/database/resource.go | 437 -- .../postgresflex/database/resource_test.go | 190 - .../postgresflex/instance/datasource.go | 222 - .../postgresflex/instance/resource.go | 757 --- .../postgresflex/instance/resource_test.go | 770 --- ...or_unknown_if_flavor_unchanged_modifier.go | 85 - .../postgresflex/postgresflex_acc_test.go | 369 -- .../services/postgresflex/user/datasource.go | 230 - .../postgresflex/user/datasource_test.go | 144 - .../services/postgresflex/user/resource.go | 577 -- .../postgresflex/user/resource_test.go | 470 -- .../services/postgresflex/utils/util.go | 31 - .../services/postgresflex/utils/util_test.go | 94 - .../rabbitmq/credential/datasource.go | 188 - .../services/rabbitmq/credential/resource.go | 423 -- .../rabbitmq/credential/resource_test.go | 280 - .../services/rabbitmq/instance/datasource.go | 260 - .../services/rabbitmq/instance/resource.go | 834 --- .../rabbitmq/instance/resource_test.go | 354 -- .../services/rabbitmq/rabbitmq_acc_test.go | 312 -- .../internal/services/rabbitmq/utils/util.go | 31 - .../services/rabbitmq/utils/util_test.go | 94 - .../services/redis/credential/datasource.go | 179 - .../services/redis/credential/resource.go | 373 -- .../redis/credential/resource_test.go | 221 - .../services/redis/instance/datasource.go | 309 -- .../services/redis/instance/resource.go | 920 ---- .../services/redis/instance/resource_test.go | 373 -- .../internal/services/redis/redis_acc_test.go | 325 -- stackit/internal/services/redis/utils/util.go | 31 - .../services/redis/utils/util_test.go | 94 - .../resourcemanager/folder/datasource.go | 183 - .../resourcemanager/folder/resource.go | 521 -- .../resourcemanager/folder/resource_test.go | 396 -- .../resourcemanager/project/datasource.go | 200 - .../resourcemanager/project/resource.go | 521 -- .../resourcemanager/project/resource_test.go | 396 -- .../resourcemanager_acc_test.go | 573 -- .../testdata/resource-folder.tf | 12 - .../testdata/resource-project.tf | 12 - .../services/resourcemanager/utils/util.go | 29 - .../resourcemanager/utils/util_test.go | 93 - .../services/scf/organization/datasource.go | 180 - .../services/scf/organization/resource.go | 558 -- .../scf/organization/resource_test.go | 177 - .../scf/organizationmanager/datasource.go | 242 - .../organizationmanager/datasource_test.go | 116 - .../scf/organizationmanager/resource.go | 484 -- .../scf/organizationmanager/resource_test.go | 233 - .../services/scf/platform/datasource.go | 223 - .../services/scf/platform/datasource_test.go | 109 - stackit/internal/services/scf/scf_acc_test.go | 456 -- .../services/scf/testdata/resource-max.tf | 23 - .../services/scf/testdata/resource-min.tf | 13 - stackit/internal/services/scf/utils/utils.go | 30 - .../internal/services/scf/utils/utils_test.go | 94 - .../secretsmanager/instance/datasource.go | 158 - .../secretsmanager/instance/resource.go | 481 -- .../secretsmanager/instance/resource_test.go | 487 -- .../secretsmanager/secretsmanager_acc_test.go | 414 -- .../secretsmanager/testdata/resource-max.tf | 34 - .../secretsmanager/testdata/resource-min.tf | 28 - .../secretsmanager/user/datasource.go | 203 - .../secretsmanager/user/datasource_test.go | 88 - .../services/secretsmanager/user/resource.go | 414 -- .../secretsmanager/user/resource_test.go | 271 - .../services/secretsmanager/utils/util.go | 31 - .../secretsmanager/utils/util_test.go | 94 - .../serverbackup/schedule/resource.go | 627 --- .../serverbackup/schedule/resource_test.go | 238 - .../schedule/schedule_datasource.go | 196 - .../schedule/schedules_datasource.go | 252 - .../schedule/schedules_datasource_test.go | 107 - .../serverbackup/serverbackup_acc_test.go | 302 - .../serverbackup/testdata/resource-max.tf | 34 - .../serverbackup/testdata/resource-min.tf | 32 - .../services/serverbackup/utils/util.go | 30 - .../services/serverbackup/utils/util_test.go | 93 - .../serverupdate/schedule/resource.go | 495 -- .../serverupdate/schedule/resource_test.go | 229 - .../schedule/schedule_datasource.go | 182 - .../schedule/schedules_datasource.go | 230 - .../schedule/schedules_datasource_test.go | 98 - .../serverupdate/serverupdate_acc_test.go | 302 - .../serverupdate/testdata/resource-max.tf | 29 - .../serverupdate/testdata/resource-min.tf | 27 - .../services/serverupdate/utils/util.go | 30 - .../services/serverupdate/utils/util_test.go | 93 - .../serviceaccount/account/datasource.go | 157 - .../serviceaccount/account/resource.go | 340 -- .../serviceaccount/account/resource_test.go | 161 - .../services/serviceaccount/key/const.go | 26 - .../services/serviceaccount/key/resource.go | 352 -- .../serviceaccount/key/resource_test.go | 124 - .../serviceaccount/serviceaccount_acc_test.go | 191 - .../services/serviceaccount/token/const.go | 26 - .../services/serviceaccount/token/resource.go | 405 -- .../serviceaccount/token/resource_test.go | 230 - .../services/serviceaccount/utils/util.go | 29 - .../serviceaccount/utils/util_test.go | 93 - .../services/serviceenablement/utils/util.go | 31 - .../serviceenablement/utils/util_test.go | 94 - .../services/ske/cluster/datasource.go | 372 -- .../internal/services/ske/cluster/resource.go | 2292 -------- .../services/ske/cluster/resource_test.go | 2639 --------- .../services/ske/kubeconfig/resource.go | 512 -- .../services/ske/kubeconfig/resource_test.go | 337 -- stackit/internal/services/ske/ske_acc_test.go | 614 --- .../services/ske/testdata/resource-max.tf | 116 - .../services/ske/testdata/resource-min.tf | 51 - stackit/internal/services/ske/utils/util.go | 29 - .../internal/services/ske/utils/util_test.go | 93 - .../sqlserverflex/instance/datasource.go | 238 - .../sqlserverflex/instance/resource.go | 899 --- .../sqlserverflex/instance/resource_test.go | 821 --- .../sqlserverflex/sqlserverflex_acc_test.go | 480 -- .../sqlserverflex/testdata/resource-max.tf | 51 - .../sqlserverflex/testdata/resource-min.tf | 33 - .../services/sqlserverflex/user/datasource.go | 235 - .../sqlserverflex/user/datasource_test.go | 144 - .../services/sqlserverflex/user/resource.go | 487 -- .../sqlserverflex/user/resource_test.go | 387 -- .../services/sqlserverflex/utils/util.go | 32 - .../services/sqlserverflex/utils/util_test.go | 94 - stackit/provider.go | 225 +- 374 files changed, 2 insertions(+), 114477 deletions(-) delete mode 100644 stackit/internal/services/access_token/access_token_acc_test.go delete mode 100644 stackit/internal/services/access_token/ephemeral_resource.go delete mode 100644 stackit/internal/services/access_token/ephemeral_resource_test.go delete mode 100644 stackit/internal/services/access_token/testdata/ephemeral_resource.tf delete mode 100644 stackit/internal/services/access_token/testdata/service_account.json delete mode 100644 stackit/internal/services/cdn/cdn_acc_test.go delete mode 100644 stackit/internal/services/cdn/customdomain/datasource.go delete mode 100644 stackit/internal/services/cdn/customdomain/datasource_test.go delete mode 100644 stackit/internal/services/cdn/customdomain/resource.go delete mode 100644 stackit/internal/services/cdn/customdomain/resource_test.go delete mode 100644 stackit/internal/services/cdn/distribution/datasource.go delete mode 100644 stackit/internal/services/cdn/distribution/resource.go delete mode 100644 stackit/internal/services/cdn/distribution/resource_test.go delete mode 100644 stackit/internal/services/cdn/utils/util.go delete mode 100644 stackit/internal/services/cdn/utils/util_test.go delete mode 100644 stackit/internal/services/dns/dns_acc_test.go delete mode 100644 stackit/internal/services/dns/recordset/datasource.go delete mode 100644 stackit/internal/services/dns/recordset/resource.go delete mode 100644 stackit/internal/services/dns/recordset/resource_test.go delete mode 100644 stackit/internal/services/dns/testdata/resource-max.tf delete mode 100644 stackit/internal/services/dns/testdata/resource-min.tf delete mode 100644 stackit/internal/services/dns/utils/util.go delete mode 100644 stackit/internal/services/dns/utils/util_test.go delete mode 100644 stackit/internal/services/dns/zone/datasource.go delete mode 100644 stackit/internal/services/dns/zone/resource.go delete mode 100644 stackit/internal/services/dns/zone/resource_test.go delete mode 100644 stackit/internal/services/git/git_acc_test.go delete mode 100644 stackit/internal/services/git/instance/datasource.go delete mode 100644 stackit/internal/services/git/instance/resource.go delete mode 100644 stackit/internal/services/git/instance/resource_test.go delete mode 100644 stackit/internal/services/git/testdata/resource-max.tf delete mode 100644 stackit/internal/services/git/testdata/resource-min.tf delete mode 100644 stackit/internal/services/git/utils/util.go delete mode 100644 stackit/internal/services/git/utils/util_test.go delete mode 100644 stackit/internal/services/iaas/affinitygroup/const.go delete mode 100644 stackit/internal/services/iaas/affinitygroup/datasource.go delete mode 100644 stackit/internal/services/iaas/affinitygroup/resource.go delete mode 100644 stackit/internal/services/iaas/affinitygroup/resource_test.go delete mode 100644 stackit/internal/services/iaas/iaas_acc_test.go delete mode 100644 stackit/internal/services/iaas/image/datasource.go delete mode 100644 stackit/internal/services/iaas/image/datasource_test.go delete mode 100644 stackit/internal/services/iaas/image/resource.go delete mode 100644 stackit/internal/services/iaas/image/resource_test.go delete mode 100644 stackit/internal/services/iaas/image/testdata/mock-image.txt delete mode 100644 stackit/internal/services/iaas/imagev2/datasource.go delete mode 100644 stackit/internal/services/iaas/imagev2/datasource_test.go delete mode 100644 stackit/internal/services/iaas/keypair/const.go delete mode 100644 stackit/internal/services/iaas/keypair/datasource.go delete mode 100644 stackit/internal/services/iaas/keypair/resource.go delete mode 100644 stackit/internal/services/iaas/keypair/resource_test.go delete mode 100644 stackit/internal/services/iaas/machinetype/datasource.go delete mode 100644 stackit/internal/services/iaas/machinetype/datasource_test.go delete mode 100644 stackit/internal/services/iaas/network/datasource.go delete mode 100644 stackit/internal/services/iaas/network/datasource_test.go delete mode 100644 stackit/internal/services/iaas/network/resource.go delete mode 100644 stackit/internal/services/iaas/network/resource_test.go delete mode 100644 stackit/internal/services/iaas/networkarea/datasource.go delete mode 100644 stackit/internal/services/iaas/networkarea/resource.go delete mode 100644 stackit/internal/services/iaas/networkarea/resource_test.go delete mode 100644 stackit/internal/services/iaas/networkarearegion/datasource.go delete mode 100644 stackit/internal/services/iaas/networkarearegion/resource.go delete mode 100644 stackit/internal/services/iaas/networkarearegion/resource_test.go delete mode 100644 stackit/internal/services/iaas/networkarearoute/datasource.go delete mode 100644 stackit/internal/services/iaas/networkarearoute/resource.go delete mode 100644 stackit/internal/services/iaas/networkarearoute/resource_test.go delete mode 100644 stackit/internal/services/iaas/networkinterface/datasource.go delete mode 100644 stackit/internal/services/iaas/networkinterface/resource.go delete mode 100644 stackit/internal/services/iaas/networkinterface/resource_test.go delete mode 100644 stackit/internal/services/iaas/networkinterfaceattach/resource.go delete mode 100644 stackit/internal/services/iaas/project/datasource.go delete mode 100644 stackit/internal/services/iaas/project/datasource_test.go delete mode 100644 stackit/internal/services/iaas/publicip/datasource.go delete mode 100644 stackit/internal/services/iaas/publicip/resource.go delete mode 100644 stackit/internal/services/iaas/publicip/resource_test.go delete mode 100644 stackit/internal/services/iaas/publicipassociate/resource.go delete mode 100644 stackit/internal/services/iaas/publicipassociate/resource_test.go delete mode 100644 stackit/internal/services/iaas/publicipranges/datasource.go delete mode 100644 stackit/internal/services/iaas/publicipranges/datasource_test.go delete mode 100644 stackit/internal/services/iaas/securitygroup/datasource.go delete mode 100644 stackit/internal/services/iaas/securitygroup/resource.go delete mode 100644 stackit/internal/services/iaas/securitygroup/resource_test.go delete mode 100644 stackit/internal/services/iaas/securitygrouprule/datasource.go delete mode 100644 stackit/internal/services/iaas/securitygrouprule/planmodifier.go delete mode 100644 stackit/internal/services/iaas/securitygrouprule/resource.go delete mode 100644 stackit/internal/services/iaas/securitygrouprule/resource_test.go delete mode 100644 stackit/internal/services/iaas/server/const.go delete mode 100644 stackit/internal/services/iaas/server/datasource.go delete mode 100644 stackit/internal/services/iaas/server/datasource_test.go delete mode 100644 stackit/internal/services/iaas/server/resource.go delete mode 100644 stackit/internal/services/iaas/server/resource_test.go delete mode 100644 stackit/internal/services/iaas/serviceaccountattach/resource.go delete mode 100644 stackit/internal/services/iaas/testdata/datasource-image-v2-variants.tf delete mode 100644 stackit/internal/services/iaas/testdata/datasource-machinetype.tf delete mode 100644 stackit/internal/services/iaas/testdata/datasource-public-ip-ranges.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-affinity-group-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-image-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-image-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-key-pair-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-key-pair-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-area-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-area-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-interface-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-interface-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-network-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-security-group-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-security-group-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-server-max-server-attachments.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-server-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-server-min.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-volume-max.tf delete mode 100644 stackit/internal/services/iaas/testdata/resource-volume-min.tf delete mode 100644 stackit/internal/services/iaas/utils/util.go delete mode 100644 stackit/internal/services/iaas/utils/util_test.go delete mode 100644 stackit/internal/services/iaas/volume/datasource.go delete mode 100644 stackit/internal/services/iaas/volume/resource.go delete mode 100644 stackit/internal/services/iaas/volume/resource_test.go delete mode 100644 stackit/internal/services/iaas/volumeattach/resource.go delete mode 100644 stackit/internal/services/iaasalpha/iaasalpha_acc_test.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/route/datasource.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/route/resource.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/route/resource_test.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/routes/datasource.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/shared/route.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/shared/route_test.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/shared/shared.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/table/datasource.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/table/resource.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/table/resource_test.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/tables/datasource.go delete mode 100644 stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go delete mode 100644 stackit/internal/services/iaasalpha/testdata/resource-routingtable-max.tf delete mode 100644 stackit/internal/services/iaasalpha/testdata/resource-routingtable-min.tf delete mode 100644 stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-max.tf delete mode 100644 stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-min.tf delete mode 100644 stackit/internal/services/iaasalpha/utils/util.go delete mode 100644 stackit/internal/services/iaasalpha/utils/util_test.go delete mode 100644 stackit/internal/services/kms/key/datasource.go delete mode 100644 stackit/internal/services/kms/key/resource.go delete mode 100644 stackit/internal/services/kms/key/resource_test.go delete mode 100644 stackit/internal/services/kms/keyring/datasource.go delete mode 100644 stackit/internal/services/kms/keyring/resource.go delete mode 100644 stackit/internal/services/kms/keyring/resource_test.go delete mode 100644 stackit/internal/services/kms/kms_acc_test.go delete mode 100644 stackit/internal/services/kms/testdata/key-max.tf delete mode 100644 stackit/internal/services/kms/testdata/key-min.tf delete mode 100644 stackit/internal/services/kms/testdata/keyring-max.tf delete mode 100644 stackit/internal/services/kms/testdata/keyring-min.tf delete mode 100644 stackit/internal/services/kms/testdata/wrapping-key-max.tf delete mode 100644 stackit/internal/services/kms/testdata/wrapping-key-min.tf delete mode 100644 stackit/internal/services/kms/utils/util.go delete mode 100644 stackit/internal/services/kms/wrapping-key/datasource.go delete mode 100644 stackit/internal/services/kms/wrapping-key/resource.go delete mode 100644 stackit/internal/services/kms/wrapping-key/resource_test.go delete mode 100644 stackit/internal/services/loadbalancer/loadbalancer/datasource.go delete mode 100644 stackit/internal/services/loadbalancer/loadbalancer/resource.go delete mode 100644 stackit/internal/services/loadbalancer/loadbalancer/resource_test.go delete mode 100644 stackit/internal/services/loadbalancer/loadbalancer_acc_test.go delete mode 100644 stackit/internal/services/loadbalancer/observability-credential/resource.go delete mode 100644 stackit/internal/services/loadbalancer/observability-credential/resource_test.go delete mode 100644 stackit/internal/services/loadbalancer/testfiles/resource-max.tf delete mode 100644 stackit/internal/services/loadbalancer/testfiles/resource-min.tf delete mode 100644 stackit/internal/services/loadbalancer/utils/util.go delete mode 100644 stackit/internal/services/loadbalancer/utils/util_test.go delete mode 100644 stackit/internal/services/logme/credential/datasource.go delete mode 100644 stackit/internal/services/logme/credential/resource.go delete mode 100644 stackit/internal/services/logme/credential/resource_test.go delete mode 100644 stackit/internal/services/logme/instance/datasource.go delete mode 100644 stackit/internal/services/logme/instance/resource.go delete mode 100644 stackit/internal/services/logme/instance/resource_test.go delete mode 100644 stackit/internal/services/logme/logme_acc_test.go delete mode 100644 stackit/internal/services/logme/testdata/resource-max.tf delete mode 100644 stackit/internal/services/logme/testdata/resource-min.tf delete mode 100644 stackit/internal/services/logme/utils/util.go delete mode 100644 stackit/internal/services/logme/utils/util_test.go delete mode 100644 stackit/internal/services/mariadb/credential/datasource.go delete mode 100644 stackit/internal/services/mariadb/credential/resource.go delete mode 100644 stackit/internal/services/mariadb/credential/resource_test.go delete mode 100644 stackit/internal/services/mariadb/instance/datasource.go delete mode 100644 stackit/internal/services/mariadb/instance/resource.go delete mode 100644 stackit/internal/services/mariadb/instance/resource_test.go delete mode 100644 stackit/internal/services/mariadb/mariadb_acc_test.go delete mode 100644 stackit/internal/services/mariadb/testfiles/resource-max.tf delete mode 100644 stackit/internal/services/mariadb/testfiles/resource-min.tf delete mode 100644 stackit/internal/services/mariadb/utils/util.go delete mode 100644 stackit/internal/services/mariadb/utils/util_test.go delete mode 100644 stackit/internal/services/modelserving/modelserving_acc_test.go delete mode 100644 stackit/internal/services/modelserving/token/description.md delete mode 100644 stackit/internal/services/modelserving/token/resource.go delete mode 100644 stackit/internal/services/modelserving/token/resource_test.go delete mode 100644 stackit/internal/services/modelserving/utils/util.go delete mode 100644 stackit/internal/services/modelserving/utils/util_test.go delete mode 100644 stackit/internal/services/mongodbflex/instance/datasource.go delete mode 100644 stackit/internal/services/mongodbflex/instance/resource.go delete mode 100644 stackit/internal/services/mongodbflex/instance/resource_test.go delete mode 100644 stackit/internal/services/mongodbflex/mongodbflex_acc_test.go delete mode 100644 stackit/internal/services/mongodbflex/user/datasource.go delete mode 100644 stackit/internal/services/mongodbflex/user/datasource_test.go delete mode 100644 stackit/internal/services/mongodbflex/user/resource.go delete mode 100644 stackit/internal/services/mongodbflex/user/resource_test.go delete mode 100644 stackit/internal/services/mongodbflex/utils/util.go delete mode 100644 stackit/internal/services/mongodbflex/utils/util_test.go delete mode 100644 stackit/internal/services/objectstorage/bucket/datasource.go delete mode 100644 stackit/internal/services/objectstorage/bucket/resource.go delete mode 100644 stackit/internal/services/objectstorage/bucket/resource_test.go delete mode 100644 stackit/internal/services/objectstorage/credential/datasource.go delete mode 100644 stackit/internal/services/objectstorage/credential/datasource_test.go delete mode 100644 stackit/internal/services/objectstorage/credential/resource.go delete mode 100644 stackit/internal/services/objectstorage/credential/resource_test.go delete mode 100644 stackit/internal/services/objectstorage/credentialsgroup/datasource.go delete mode 100644 stackit/internal/services/objectstorage/credentialsgroup/resource.go delete mode 100644 stackit/internal/services/objectstorage/credentialsgroup/resource_test.go delete mode 100644 stackit/internal/services/objectstorage/objectstorage_acc_test.go delete mode 100644 stackit/internal/services/objectstorage/testfiles/resource-min.tf delete mode 100644 stackit/internal/services/objectstorage/utils/util.go delete mode 100644 stackit/internal/services/objectstorage/utils/util_test.go delete mode 100644 stackit/internal/services/observability/alertgroup/datasource.go delete mode 100644 stackit/internal/services/observability/alertgroup/resource.go delete mode 100644 stackit/internal/services/observability/alertgroup/resource_test.go delete mode 100644 stackit/internal/services/observability/credential/resource.go delete mode 100644 stackit/internal/services/observability/credential/resource_test.go delete mode 100644 stackit/internal/services/observability/instance/datasource.go delete mode 100644 stackit/internal/services/observability/instance/resource.go delete mode 100644 stackit/internal/services/observability/instance/resource_test.go delete mode 100644 stackit/internal/services/observability/log-alertgroup/datasource.go delete mode 100644 stackit/internal/services/observability/log-alertgroup/resource.go delete mode 100644 stackit/internal/services/observability/log-alertgroup/resource_test.go delete mode 100644 stackit/internal/services/observability/observability_acc_test.go delete mode 100644 stackit/internal/services/observability/scrapeconfig/datasource.go delete mode 100644 stackit/internal/services/observability/scrapeconfig/resource.go delete mode 100644 stackit/internal/services/observability/scrapeconfig/resource_test.go delete mode 100644 stackit/internal/services/observability/testdata/resource-max.tf delete mode 100644 stackit/internal/services/observability/testdata/resource-min.tf delete mode 100644 stackit/internal/services/observability/utils/util.go delete mode 100644 stackit/internal/services/observability/utils/util_test.go delete mode 100644 stackit/internal/services/opensearch/credential/datasource.go delete mode 100644 stackit/internal/services/opensearch/credential/resource.go delete mode 100644 stackit/internal/services/opensearch/credential/resource_test.go delete mode 100644 stackit/internal/services/opensearch/instance/datasource.go delete mode 100644 stackit/internal/services/opensearch/instance/resource.go delete mode 100644 stackit/internal/services/opensearch/instance/resource_test.go delete mode 100644 stackit/internal/services/opensearch/opensearch_acc_test.go delete mode 100644 stackit/internal/services/opensearch/utils/util.go delete mode 100644 stackit/internal/services/opensearch/utils/util_test.go delete mode 100644 stackit/internal/services/postgresflex/database/datasource.go delete mode 100644 stackit/internal/services/postgresflex/database/resource.go delete mode 100644 stackit/internal/services/postgresflex/database/resource_test.go delete mode 100644 stackit/internal/services/postgresflex/instance/datasource.go delete mode 100644 stackit/internal/services/postgresflex/instance/resource.go delete mode 100644 stackit/internal/services/postgresflex/instance/resource_test.go delete mode 100644 stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go delete mode 100644 stackit/internal/services/postgresflex/postgresflex_acc_test.go delete mode 100644 stackit/internal/services/postgresflex/user/datasource.go delete mode 100644 stackit/internal/services/postgresflex/user/datasource_test.go delete mode 100644 stackit/internal/services/postgresflex/user/resource.go delete mode 100644 stackit/internal/services/postgresflex/user/resource_test.go delete mode 100644 stackit/internal/services/postgresflex/utils/util.go delete mode 100644 stackit/internal/services/postgresflex/utils/util_test.go delete mode 100644 stackit/internal/services/rabbitmq/credential/datasource.go delete mode 100644 stackit/internal/services/rabbitmq/credential/resource.go delete mode 100644 stackit/internal/services/rabbitmq/credential/resource_test.go delete mode 100644 stackit/internal/services/rabbitmq/instance/datasource.go delete mode 100644 stackit/internal/services/rabbitmq/instance/resource.go delete mode 100644 stackit/internal/services/rabbitmq/instance/resource_test.go delete mode 100644 stackit/internal/services/rabbitmq/rabbitmq_acc_test.go delete mode 100644 stackit/internal/services/rabbitmq/utils/util.go delete mode 100644 stackit/internal/services/rabbitmq/utils/util_test.go delete mode 100644 stackit/internal/services/redis/credential/datasource.go delete mode 100644 stackit/internal/services/redis/credential/resource.go delete mode 100644 stackit/internal/services/redis/credential/resource_test.go delete mode 100644 stackit/internal/services/redis/instance/datasource.go delete mode 100644 stackit/internal/services/redis/instance/resource.go delete mode 100644 stackit/internal/services/redis/instance/resource_test.go delete mode 100644 stackit/internal/services/redis/redis_acc_test.go delete mode 100644 stackit/internal/services/redis/utils/util.go delete mode 100644 stackit/internal/services/redis/utils/util_test.go delete mode 100644 stackit/internal/services/resourcemanager/folder/datasource.go delete mode 100644 stackit/internal/services/resourcemanager/folder/resource.go delete mode 100644 stackit/internal/services/resourcemanager/folder/resource_test.go delete mode 100644 stackit/internal/services/resourcemanager/project/datasource.go delete mode 100644 stackit/internal/services/resourcemanager/project/resource.go delete mode 100644 stackit/internal/services/resourcemanager/project/resource_test.go delete mode 100644 stackit/internal/services/resourcemanager/resourcemanager_acc_test.go delete mode 100644 stackit/internal/services/resourcemanager/testdata/resource-folder.tf delete mode 100644 stackit/internal/services/resourcemanager/testdata/resource-project.tf delete mode 100644 stackit/internal/services/resourcemanager/utils/util.go delete mode 100644 stackit/internal/services/resourcemanager/utils/util_test.go delete mode 100644 stackit/internal/services/scf/organization/datasource.go delete mode 100644 stackit/internal/services/scf/organization/resource.go delete mode 100644 stackit/internal/services/scf/organization/resource_test.go delete mode 100644 stackit/internal/services/scf/organizationmanager/datasource.go delete mode 100644 stackit/internal/services/scf/organizationmanager/datasource_test.go delete mode 100644 stackit/internal/services/scf/organizationmanager/resource.go delete mode 100644 stackit/internal/services/scf/organizationmanager/resource_test.go delete mode 100644 stackit/internal/services/scf/platform/datasource.go delete mode 100644 stackit/internal/services/scf/platform/datasource_test.go delete mode 100644 stackit/internal/services/scf/scf_acc_test.go delete mode 100644 stackit/internal/services/scf/testdata/resource-max.tf delete mode 100644 stackit/internal/services/scf/testdata/resource-min.tf delete mode 100644 stackit/internal/services/scf/utils/utils.go delete mode 100644 stackit/internal/services/scf/utils/utils_test.go delete mode 100644 stackit/internal/services/secretsmanager/instance/datasource.go delete mode 100644 stackit/internal/services/secretsmanager/instance/resource.go delete mode 100644 stackit/internal/services/secretsmanager/instance/resource_test.go delete mode 100644 stackit/internal/services/secretsmanager/secretsmanager_acc_test.go delete mode 100644 stackit/internal/services/secretsmanager/testdata/resource-max.tf delete mode 100644 stackit/internal/services/secretsmanager/testdata/resource-min.tf delete mode 100644 stackit/internal/services/secretsmanager/user/datasource.go delete mode 100644 stackit/internal/services/secretsmanager/user/datasource_test.go delete mode 100644 stackit/internal/services/secretsmanager/user/resource.go delete mode 100644 stackit/internal/services/secretsmanager/user/resource_test.go delete mode 100644 stackit/internal/services/secretsmanager/utils/util.go delete mode 100644 stackit/internal/services/secretsmanager/utils/util_test.go delete mode 100644 stackit/internal/services/serverbackup/schedule/resource.go delete mode 100644 stackit/internal/services/serverbackup/schedule/resource_test.go delete mode 100644 stackit/internal/services/serverbackup/schedule/schedule_datasource.go delete mode 100644 stackit/internal/services/serverbackup/schedule/schedules_datasource.go delete mode 100644 stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go delete mode 100644 stackit/internal/services/serverbackup/serverbackup_acc_test.go delete mode 100644 stackit/internal/services/serverbackup/testdata/resource-max.tf delete mode 100644 stackit/internal/services/serverbackup/testdata/resource-min.tf delete mode 100644 stackit/internal/services/serverbackup/utils/util.go delete mode 100644 stackit/internal/services/serverbackup/utils/util_test.go delete mode 100644 stackit/internal/services/serverupdate/schedule/resource.go delete mode 100644 stackit/internal/services/serverupdate/schedule/resource_test.go delete mode 100644 stackit/internal/services/serverupdate/schedule/schedule_datasource.go delete mode 100644 stackit/internal/services/serverupdate/schedule/schedules_datasource.go delete mode 100644 stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go delete mode 100644 stackit/internal/services/serverupdate/serverupdate_acc_test.go delete mode 100644 stackit/internal/services/serverupdate/testdata/resource-max.tf delete mode 100644 stackit/internal/services/serverupdate/testdata/resource-min.tf delete mode 100644 stackit/internal/services/serverupdate/utils/util.go delete mode 100644 stackit/internal/services/serverupdate/utils/util_test.go delete mode 100644 stackit/internal/services/serviceaccount/account/datasource.go delete mode 100644 stackit/internal/services/serviceaccount/account/resource.go delete mode 100644 stackit/internal/services/serviceaccount/account/resource_test.go delete mode 100644 stackit/internal/services/serviceaccount/key/const.go delete mode 100644 stackit/internal/services/serviceaccount/key/resource.go delete mode 100644 stackit/internal/services/serviceaccount/key/resource_test.go delete mode 100644 stackit/internal/services/serviceaccount/serviceaccount_acc_test.go delete mode 100644 stackit/internal/services/serviceaccount/token/const.go delete mode 100644 stackit/internal/services/serviceaccount/token/resource.go delete mode 100644 stackit/internal/services/serviceaccount/token/resource_test.go delete mode 100644 stackit/internal/services/serviceaccount/utils/util.go delete mode 100644 stackit/internal/services/serviceaccount/utils/util_test.go delete mode 100644 stackit/internal/services/serviceenablement/utils/util.go delete mode 100644 stackit/internal/services/serviceenablement/utils/util_test.go delete mode 100644 stackit/internal/services/ske/cluster/datasource.go delete mode 100644 stackit/internal/services/ske/cluster/resource.go delete mode 100644 stackit/internal/services/ske/cluster/resource_test.go delete mode 100644 stackit/internal/services/ske/kubeconfig/resource.go delete mode 100644 stackit/internal/services/ske/kubeconfig/resource_test.go delete mode 100644 stackit/internal/services/ske/ske_acc_test.go delete mode 100644 stackit/internal/services/ske/testdata/resource-max.tf delete mode 100644 stackit/internal/services/ske/testdata/resource-min.tf delete mode 100644 stackit/internal/services/ske/utils/util.go delete mode 100644 stackit/internal/services/ske/utils/util_test.go delete mode 100644 stackit/internal/services/sqlserverflex/instance/datasource.go delete mode 100644 stackit/internal/services/sqlserverflex/instance/resource.go delete mode 100644 stackit/internal/services/sqlserverflex/instance/resource_test.go delete mode 100644 stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go delete mode 100644 stackit/internal/services/sqlserverflex/testdata/resource-max.tf delete mode 100644 stackit/internal/services/sqlserverflex/testdata/resource-min.tf delete mode 100644 stackit/internal/services/sqlserverflex/user/datasource.go delete mode 100644 stackit/internal/services/sqlserverflex/user/datasource_test.go delete mode 100644 stackit/internal/services/sqlserverflex/user/resource.go delete mode 100644 stackit/internal/services/sqlserverflex/user/resource_test.go delete mode 100644 stackit/internal/services/sqlserverflex/utils/util.go delete mode 100644 stackit/internal/services/sqlserverflex/utils/util_test.go diff --git a/stackit/internal/services/access_token/access_token_acc_test.go b/stackit/internal/services/access_token/access_token_acc_test.go deleted file mode 100644 index d544fe39..00000000 --- a/stackit/internal/services/access_token/access_token_acc_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package access_token_test - -import ( - _ "embed" - "regexp" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/statecheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" - "github.com/hashicorp/terraform-plugin-testing/tfversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -//go:embed testdata/ephemeral_resource.tf -var ephemeralResourceConfig string - -var testConfigVars = config.Variables{ - "default_region": config.StringVariable(testutil.Region), -} - -func TestAccEphemeralAccessToken(t *testing.T) { - resource.Test(t, resource.TestCase{ - TerraformVersionChecks: []tfversion.TerraformVersionCheck{ - tfversion.SkipBelow(tfversion.Version1_10_0), - }, - ProtoV6ProviderFactories: testutil.TestEphemeralAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - { - Config: ephemeralResourceConfig, - ConfigVariables: testConfigVars, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue( - "echo.example", - tfjsonpath.New("data").AtMapKey("access_token"), - knownvalue.NotNull(), - ), - // JWT access tokens start with "ey" because the first part is base64-encoded JSON that begins with "{". - statecheck.ExpectKnownValue( - "echo.example", - tfjsonpath.New("data").AtMapKey("access_token"), - knownvalue.StringRegexp(regexp.MustCompile(`^ey`)), - ), - }, - }, - }, - }) -} diff --git a/stackit/internal/services/access_token/ephemeral_resource.go b/stackit/internal/services/access_token/ephemeral_resource.go deleted file mode 100644 index 8ae346ba..00000000 --- a/stackit/internal/services/access_token/ephemeral_resource.go +++ /dev/null @@ -1,132 +0,0 @@ -package access_token - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/ephemeral" - "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/auth" - "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" -) - -var ( - _ ephemeral.EphemeralResource = &accessTokenEphemeralResource{} - _ ephemeral.EphemeralResourceWithConfigure = &accessTokenEphemeralResource{} -) - -func NewAccessTokenEphemeralResource() ephemeral.EphemeralResource { - return &accessTokenEphemeralResource{} -} - -type accessTokenEphemeralResource struct { - keyAuthConfig config.Configuration -} - -func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) { - ephemeralProviderData, ok := conversion.ParseEphemeralProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled( - ctx, - &ephemeralProviderData.ProviderData, - &resp.Diagnostics, - "stackit_access_token", "ephemeral_resource", - ) - if resp.Diagnostics.HasError() { - return - } - - e.keyAuthConfig = config.Configuration{ - ServiceAccountKey: ephemeralProviderData.ServiceAccountKey, - ServiceAccountKeyPath: ephemeralProviderData.ServiceAccountKeyPath, - PrivateKeyPath: ephemeralProviderData.PrivateKey, - PrivateKey: ephemeralProviderData.PrivateKeyPath, - TokenCustomUrl: ephemeralProviderData.TokenCustomEndpoint, - } -} - -type ephemeralTokenModel struct { - AccessToken types.String `tfsdk:"access_token"` -} - -func (e *accessTokenEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_access_token" -} - -func (e *accessTokenEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { - description := features.AddBetaDescription( - fmt.Sprintf( - "%s\n\n%s", - "Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. "+ - "A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. "+ - "If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. "+ - "Access tokens generated from service account keys expire after 60 minutes.", - "~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). "+ - "If any other authentication method is configured, this ephemeral resource will fail with an error.", - ), - core.EphemeralResource, - ) - - resp.Schema = schema.Schema{ - Description: description, - Attributes: map[string]schema.Attribute{ - "access_token": schema.StringAttribute{ - Description: "JWT access token for STACKIT API authentication.", - Computed: true, - Sensitive: true, - }, - }, - } -} - -func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { - var model ephemeralTokenModel - - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - accessToken, err := getAccessToken(&e.keyAuthConfig) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Access token generation failed", err.Error()) - return - } - - model.AccessToken = types.StringValue(accessToken) - resp.Diagnostics.Append(resp.Result.Set(ctx, model)...) -} - -// getAccessToken initializes authentication using the provided config and returns an access token via the KeyFlow mechanism. -func getAccessToken(keyAuthConfig *config.Configuration) (string, error) { - roundTripper, err := auth.KeyAuth(keyAuthConfig) - if err != nil { - return "", fmt.Errorf( - "failed to initialize authentication: %w. "+ - "Make sure service account credentials are configured either in the provider configuration or via environment variables", - err, - ) - } - - // Type assert to access token functionality - client, ok := roundTripper.(*clients.KeyFlow) - if !ok { - return "", fmt.Errorf("internal error: expected *clients.KeyFlow, but received a different implementation of http.RoundTripper") - } - - // Retrieve the access token - accessToken, err := client.GetAccessToken() - if err != nil { - return "", fmt.Errorf("error obtaining access token: %w", err) - } - - return accessToken, nil -} diff --git a/stackit/internal/services/access_token/ephemeral_resource_test.go b/stackit/internal/services/access_token/ephemeral_resource_test.go deleted file mode 100644 index 5df2b91c..00000000 --- a/stackit/internal/services/access_token/ephemeral_resource_test.go +++ /dev/null @@ -1,253 +0,0 @@ -package access_token - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/x509" - _ "embed" - "encoding/json" - "encoding/pem" - "net/http" - "net/http/httptest" - "os" - "testing" - "time" - - "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" -) - -//go:embed testdata/service_account.json -var testServiceAccountKey string - -func startMockTokenServer() *httptest.Server { - handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - resp := clients.TokenResponseBody{ - AccessToken: "mock_access_token", - RefreshToken: "mock_refresh_token", - TokenType: "Bearer", - ExpiresIn: int(time.Now().Add(time.Hour).Unix()), - Scope: "mock_scope", - } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(resp) - }) - return httptest.NewServer(handler) -} - -func generatePrivateKey() (string, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return "", err - } - privateKeyPEM := &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - } - return string(pem.EncodeToMemory(privateKeyPEM)), nil -} - -func writeTempPEMFile(t *testing.T, pemContent string) string { - t.Helper() - - tmpFile, err := os.CreateTemp("", "stackit_test_private_key_*.pem") - if err != nil { - t.Fatal(err) - } - - if _, err := tmpFile.WriteString(pemContent); err != nil { - t.Fatal(err) - } - - if err := tmpFile.Close(); err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - _ = os.Remove(tmpFile.Name()) - }) - - return tmpFile.Name() -} - -func TestGetAccessToken(t *testing.T) { - mockServer := startMockTokenServer() - t.Cleanup(mockServer.Close) - - privateKey, err := generatePrivateKey() - if err != nil { - t.Fatal(err) - } - - tests := []struct { - description string - setupEnv func() - cleanupEnv func() - cfgFactory func() *config.Configuration - expectError bool - expected string - }{ - { - description: "should return token when service account key passed by value", - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - ServiceAccountKey: testServiceAccountKey, - PrivateKey: privateKey, - TokenCustomUrl: mockServer.URL, - } - }, - expectError: false, - expected: "mock_access_token", - }, - { - description: "should return token when service account key is loaded from file path", - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - ServiceAccountKeyPath: "testdata/service_account.json", - PrivateKey: privateKey, - TokenCustomUrl: mockServer.URL, - } - }, - expectError: false, - expected: "mock_access_token", - }, - { - description: "should fail when private key is invalid", - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - ServiceAccountKey: "invalid-json", - PrivateKey: "invalid-PEM", - TokenCustomUrl: mockServer.URL, - } - }, - expectError: true, - expected: "", - }, - { - description: "should return token when service account key is set via env", - setupEnv: func() { - _ = os.Setenv("STACKIT_SERVICE_ACCOUNT_KEY", testServiceAccountKey) - }, - cleanupEnv: func() { - _ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY") - }, - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - PrivateKey: privateKey, - TokenCustomUrl: mockServer.URL, - } - }, - expectError: false, - expected: "mock_access_token", - }, - { - description: "should return token when service account key path is set via env", - setupEnv: func() { - _ = os.Setenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH", "testdata/service_account.json") - }, - cleanupEnv: func() { - _ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH") - }, - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - PrivateKey: privateKey, - TokenCustomUrl: mockServer.URL, - } - }, - expectError: false, - expected: "mock_access_token", - }, - { - description: "should return token when private key is set via env", - setupEnv: func() { - _ = os.Setenv("STACKIT_PRIVATE_KEY", privateKey) - }, - cleanupEnv: func() { - _ = os.Unsetenv("STACKIT_PRIVATE_KEY") - }, - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - ServiceAccountKey: testServiceAccountKey, - TokenCustomUrl: mockServer.URL, - } - }, - expectError: false, - expected: "mock_access_token", - }, - { - description: "should return token when private key path is set via env", - setupEnv: func() { - // Write temp file and set env - tmpFile := writeTempPEMFile(t, privateKey) - _ = os.Setenv("STACKIT_PRIVATE_KEY_PATH", tmpFile) - }, - cleanupEnv: func() { - _ = os.Unsetenv("STACKIT_PRIVATE_KEY_PATH") - }, - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - ServiceAccountKey: testServiceAccountKey, - TokenCustomUrl: mockServer.URL, - } - }, - expectError: false, - expected: "mock_access_token", - }, - { - description: "should fail when no service account key or private key is set", - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - TokenCustomUrl: mockServer.URL, - } - }, - expectError: true, - expected: "", - }, - { - description: "should fail when no service account key or private key is set via env", - setupEnv: func() { - _ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY") - _ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH") - _ = os.Unsetenv("STACKIT_PRIVATE_KEY") - _ = os.Unsetenv("STACKIT_PRIVATE_KEY_PATH") - }, - cleanupEnv: func() { - // Restore original environment variables - }, - cfgFactory: func() *config.Configuration { - return &config.Configuration{ - TokenCustomUrl: mockServer.URL, - } - }, - expectError: true, - expected: "", - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - if tt.setupEnv != nil { - tt.setupEnv() - } - if tt.cleanupEnv != nil { - defer tt.cleanupEnv() - } - - cfg := tt.cfgFactory() - - token, err := getAccessToken(cfg) - if tt.expectError { - if err == nil { - t.Errorf("expected error but got none for test case '%s'", tt.description) - } - } else { - if err != nil { - t.Errorf("did not expect error but got: %v for test case '%s'", err, tt.description) - } - if token != tt.expected { - t.Errorf("expected token '%s', got '%s' for test case '%s'", tt.expected, token, tt.description) - } - } - }) - } -} diff --git a/stackit/internal/services/access_token/testdata/ephemeral_resource.tf b/stackit/internal/services/access_token/testdata/ephemeral_resource.tf deleted file mode 100644 index 3d5731a3..00000000 --- a/stackit/internal/services/access_token/testdata/ephemeral_resource.tf +++ /dev/null @@ -1,15 +0,0 @@ -variable "default_region" {} - -provider "stackit" { - default_region = var.default_region - enable_beta_resources = true -} - -ephemeral "stackit_access_token" "example" {} - -provider "echo" { - data = ephemeral.stackit_access_token.example -} - -resource "echo" "example" { -} diff --git a/stackit/internal/services/access_token/testdata/service_account.json b/stackit/internal/services/access_token/testdata/service_account.json deleted file mode 100644 index 62df6f44..00000000 --- a/stackit/internal/services/access_token/testdata/service_account.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697", - "publicKey": "-----BEGIN PUBLIC KEY-----\nABC\n-----END PUBLIC KEY-----", - "createdAt": "2025-11-25T15:19:30.689+00:00", - "keyType": "USER_MANAGED", - "keyOrigin": "GENERATED", - "keyAlgorithm": "RSA_2048", - "active": true, - "credentials": { - "kid": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697", - "iss": "foo.bar@sa.stackit.cloud", - "sub": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697", - "aud": "https://stackit-service-account-prod.apps.01.cf.eu01.stackit.cloud", - "privateKey": "-----BEGIN PRIVATE KEY-----\nABC\n-----END PRIVATE KEY-----" - } -} \ No newline at end of file diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go deleted file mode 100644 index 0dd031a5..00000000 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ /dev/null @@ -1,407 +0,0 @@ -package cdn_test - -import ( - "context" - cryptoRand "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "math/big" - "net" - "strings" - "testing" - "time" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" - "github.com/stackitcloud/stackit-sdk-go/services/cdn/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "config_backend_type": "http", - "config_backend_origin_url": "https://test-backend-1.cdn-dev.runs.onstackit.cloud", - "config_regions": "\"EU\", \"US\"", - "config_regions_updated": "\"EU\", \"US\", \"ASIA\"", - "blocked_countries": "\"CU\", \"AQ\"", // Do NOT use DE or AT here, because the request might be blocked by bunny at the time of creation - don't lock yourself out - "custom_domain_prefix": uuid.NewString(), // we use a different domain prefix each test run due to inconsistent upstream release of domains, which might impair consecutive test runs - "dns_name": fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]), -} - -func configResources(regions string, geofencingCountries []string) string { - var quotedCountries []string - for _, country := range geofencingCountries { - quotedCountries = append(quotedCountries, fmt.Sprintf(`%q`, country)) - } - - geofencingList := strings.Join(quotedCountries, ",") - return fmt.Sprintf(` - %s - - resource "stackit_cdn_distribution" "distribution" { - project_id = "%s" - config = { - backend = { - type = "http" - origin_url = "%s" - geofencing = { - "%s" = [%s] - } - } - regions = [%s] - blocked_countries = [%s] - - optimizer = { - enabled = true - } - } - } - - resource "stackit_dns_zone" "dns_zone" { - project_id = "%s" - name = "cdn_acc_test_zone" - dns_name = "%s" - contact_email = "aa@bb.cc" - type = "primary" - default_ttl = 3600 - } - resource "stackit_dns_record_set" "dns_record" { - project_id = "%s" - zone_id = stackit_dns_zone.dns_zone.zone_id - name = "%s" - type = "CNAME" - records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] - } - `, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], instanceResource["config_backend_origin_url"], geofencingList, - regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"], - testutil.ProjectId, instanceResource["custom_domain_prefix"]) -} - -func configCustomDomainResources(regions, cert, key string, geofencingCountries []string) string { - return fmt.Sprintf(` - %s - - resource "stackit_cdn_custom_domain" "custom_domain" { - project_id = stackit_cdn_distribution.distribution.project_id - distribution_id = stackit_cdn_distribution.distribution.distribution_id - name = "${stackit_dns_record_set.dns_record.name}.${stackit_dns_zone.dns_zone.dns_name}" - certificate = { - certificate = %q - private_key = %q - } - } -`, configResources(regions, geofencingCountries), cert, key) -} - -func configDatasources(regions, cert, key string, geofencingCountries []string) string { - return fmt.Sprintf(` - %s - - data "stackit_cdn_distribution" "distribution" { - project_id = stackit_cdn_distribution.distribution.project_id - distribution_id = stackit_cdn_distribution.distribution.distribution_id - } - - data "stackit_cdn_custom_domain" "custom_domain" { - project_id = stackit_cdn_custom_domain.custom_domain.project_id - distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id - name = stackit_cdn_custom_domain.custom_domain.name - - } - `, configCustomDomainResources(regions, cert, key, geofencingCountries)) -} -func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { - privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048) - if err != nil { - t.Fatalf("failed to generate key: %s", err.Error()) - } - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Issuer: pkix.Name{CommonName: organization}, - Subject: pkix.Name{ - Organization: []string{organization}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour), - - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - cert, err = x509.CreateCertificate( - cryptoRand.Reader, - &template, - &template, - &privateKey.PublicKey, - privateKey, - ) - if err != nil { - t.Fatalf("failed to generate cert: %s", err.Error()) - } - - return pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert, - }), pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - }) -} -func TestAccCDNDistributionResource(t *testing.T) { - fullDomainName := fmt.Sprintf("%s.%s", instanceResource["custom_domain_prefix"], instanceResource["dns_name"]) - organization := fmt.Sprintf("organization-%s", uuid.NewString()) - cert, key := makeCertAndKey(t, organization) - geofencing := []string{"DE", "ES"} - - organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString()) - cert_updated, key_updated := makeCertAndKey(t, organization_updated) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckCDNDistributionDestroy, - Steps: []resource.TestStep{ - // Distribution Create - { - Config: configResources(instanceResource["config_regions"], geofencing), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "1"), - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "2"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), - resource.TestCheckResourceAttr( - "stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]), - "DE", - ), - resource.TestCheckResourceAttr( - "stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]), - "ES", - ), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), - ), - }, - // Wait step, that confirms the CNAME record has "propagated" - { - Config: configResources(instanceResource["config_regions"], geofencing), - Check: func(_ *terraform.State) error { - _, err := blockUntilDomainResolves(fullDomainName) - return err - }, - }, - // Custom Domain Create - { - Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key), geofencing), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), - resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), - resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"), - resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"), - ), - }, - // Import - { - ResourceName: "stackit_cdn_distribution.distribution", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_cdn_distribution.distribution"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_cdn_distribution.distribution") - } - distributionId, ok := r.Primary.Attributes["distribution_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute distribution_id") - } - - return fmt.Sprintf("%s,%s", testutil.ProjectId, distributionId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"domains"}, // we added a domain in the meantime... - }, - { - ResourceName: "stackit_cdn_custom_domain.custom_domain", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_cdn_custom_domain.custom_domain"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_cdn_custom_domain.custom_domain") - } - distributionId, ok := r.Primary.Attributes["distribution_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute distribution_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, distributionId, name), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - "certificate.certificate", - "certificate.private_key", - }, - }, - // Data Source - { - Config: configDatasources(instanceResource["config_regions"], string(cert), string(key), geofencing), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "distribution_id"), - resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "updated_at"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.#", "2"), - resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "domains.0.name"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.name", fullDomainName), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.type", "managed"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.type", "custom"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.#", "2"), - resource.TestCheckResourceAttr( - "data.stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]), - "DE", - ), - resource.TestCheckResourceAttr( - "data.stackit_cdn_distribution.distribution", - fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]), - "ES", - ), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.0", "EU"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.1", "US"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"), - resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), - resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"), - resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), - resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"), - ), - }, - // Update - { - Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated), geofencing), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "2"), - resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.name", fullDomainName), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.type", "custom"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "3"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.2", "ASIA"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), - resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), - resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.version", "2"), - resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), - resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"), - resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"), - ), - }, - }, - }) -} -func testAccCheckCDNDistributionDestroy(s *terraform.State) error { - ctx := context.Background() - var client *cdn.APIClient - var err error - if testutil.MongoDBFlexCustomEndpoint == "" { - client, err = cdn.NewAPIClient() - } else { - client, err = cdn.NewAPIClient( - config.WithEndpoint(testutil.MongoDBFlexCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - distributionsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_mongodbflex_instance" { - continue - } - distributionId := strings.Split(rs.Primary.ID, core.Separator)[1] - distributionsToDestroy = append(distributionsToDestroy, distributionId) - } - - for _, dist := range distributionsToDestroy { - _, err := client.DeleteDistribution(ctx, testutil.ProjectId, dist).Execute() - if err != nil { - return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: %w", dist, err) - } - _, err = wait.DeleteDistributionWaitHandler(ctx, client, testutil.ProjectId, dist).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: waiting for deletion %w", dist, err) - } - } - return nil -} - -const ( - recordCheckInterval time.Duration = 3 * time.Second - recordCheckAttempts = 100 // wait up to 5 minutes for record to be come available (normally takes less than 2 minutes) -) - -func blockUntilDomainResolves(domain string) (net.IP, error) { - // wait until it becomes ready - isReady := func() (net.IP, error) { - ips, err := net.LookupIP(domain) - if err != nil { - return nil, fmt.Errorf("error looking up IP for domain %s: %w", domain, err) - } - for _, ip := range ips { - if ip.String() != "" { - return ip, nil - } - } - return nil, fmt.Errorf("no IP for domain: %v", domain) - } - return retry(recordCheckAttempts, recordCheckInterval, isReady) -} - -func retry[T any](attempts int, sleep time.Duration, f func() (T, error)) (T, error) { - var zero T - var errOuter error - for i := 0; i < attempts; i++ { - dist, err := f() - if err == nil { - return dist, nil - } - errOuter = err - time.Sleep(sleep) - } - return zero, fmt.Errorf("retry timed out, last error: %w", errOuter) -} diff --git a/stackit/internal/services/cdn/customdomain/datasource.go b/stackit/internal/services/cdn/customdomain/datasource.go deleted file mode 100644 index 6f5dfa79..00000000 --- a/stackit/internal/services/cdn/customdomain/datasource.go +++ /dev/null @@ -1,236 +0,0 @@ -package cdn - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &customDomainDataSource{} - _ datasource.DataSourceWithConfigure = &customDomainDataSource{} -) - -var certificateDataSourceTypes = map[string]attr.Type{ - "version": types.Int64Type, -} - -type customDomainDataSource struct { - client *cdn.APIClient -} - -func NewCustomDomainDataSource() datasource.DataSource { - return &customDomainDataSource{} -} - -type customDomainDataSourceModel struct { - ID types.String `tfsdk:"id"` - DistributionId types.String `tfsdk:"distribution_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - Status types.String `tfsdk:"status"` - Errors types.List `tfsdk:"errors"` - Certificate types.Object `tfsdk:"certificate"` -} - -func (d *customDomainDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", core.Datasource) - if resp.Diagnostics.HasError() { - return - } - - apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "CDN client configured") -} - -func (r *customDomainDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain" -} - -func (r *customDomainDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Datasource), - Description: "CDN distribution data source schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["id"], - Computed: true, - }, - "name": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["name"], - Required: true, - }, - "distribution_id": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["distribution_id"], - Required: true, - Validators: []validator.String{validate.UUID()}, - }, - "project_id": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["project_id"], - Required: true, - }, - "status": schema.StringAttribute{ - Computed: true, - Description: customDomainSchemaDescriptions["status"], - }, - "errors": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - Description: customDomainSchemaDescriptions["errors"], - }, - "certificate": schema.SingleNestedAttribute{ - Description: certificateSchemaDescriptions["main"], - Optional: true, - Attributes: map[string]schema.Attribute{ - "version": schema.Int64Attribute{ - Description: certificateSchemaDescriptions["version"], - Computed: true, - }, - }, - }, - }, - } -} - -func (r *customDomainDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model customDomainDataSourceModel // Use the new data source model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - name := model.Name.ValueString() - ctx = tflog.SetField(ctx, "name", name) - - customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Call the new data source mapping function - err = mapCustomDomainDataSourceFields(customDomainResp, &model, projectId, distributionId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "CDN custom domain read") -} - -func mapCustomDomainDataSourceFields(customDomainResponse *cdn.GetCustomDomainResponse, model *customDomainDataSourceModel, projectId, distributionId string) error { - if customDomainResponse == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if customDomainResponse.CustomDomain == nil { - return fmt.Errorf("CustomDomain is missing in response") - } - - if customDomainResponse.CustomDomain.Name == nil { - return fmt.Errorf("name is missing in response") - } - if customDomainResponse.CustomDomain.Status == nil { - return fmt.Errorf("status missing in response") - } - - normalizedCert, err := normalizeCertificate(customDomainResponse.Certificate) - if err != nil { - return fmt.Errorf("Certificate error in normalizer: %w", err) - } - - // If the certificate is managed, the certificate block in the state should be null. - if normalizedCert.Type == "managed" { - model.Certificate = types.ObjectNull(certificateDataSourceTypes) - } else { - // For custom certificates, we only care about the version. - version := types.Int64Null() - if normalizedCert.Version != nil { - version = types.Int64Value(*normalizedCert.Version) - } - - certificateObj, diags := types.ObjectValue(certificateDataSourceTypes, map[string]attr.Value{ - "version": version, - }) - if diags.HasError() { - return fmt.Errorf("failed to map certificate: %w", core.DiagsToError(diags)) - } - model.Certificate = certificateObj - } - - model.ID = types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, distributionId, *customDomainResponse.CustomDomain.Name)) - model.Status = types.StringValue(string(*customDomainResponse.CustomDomain.Status)) - - customDomainErrors := []attr.Value{} - if customDomainResponse.CustomDomain.Errors != nil { - for _, e := range *customDomainResponse.CustomDomain.Errors { - if e.En == nil { - return fmt.Errorf("error description missing") - } - customDomainErrors = append(customDomainErrors, types.StringValue(*e.En)) - } - } - modelErrors, diags := types.ListValue(types.StringType, customDomainErrors) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.Errors = modelErrors - - // Also map the fields back to the model from the config - model.ProjectId = types.StringValue(projectId) - model.DistributionId = types.StringValue(distributionId) - model.Name = types.StringValue(*customDomainResponse.CustomDomain.Name) - - return nil -} diff --git a/stackit/internal/services/cdn/customdomain/datasource_test.go b/stackit/internal/services/cdn/customdomain/datasource_test.go deleted file mode 100644 index 0a823b27..00000000 --- a/stackit/internal/services/cdn/customdomain/datasource_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package cdn - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" -) - -func TestMapDataSourceFields(t *testing.T) { - emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{}) - - // Expected certificate object when a custom certificate is returned - certAttributes := map[string]attr.Value{ - "version": types.Int64Value(3), - } - certificateObj, _ := types.ObjectValue(certificateDataSourceTypes, certAttributes) - - // Helper to create expected model instances - expectedModel := func(mods ...func(*customDomainDataSourceModel)) *customDomainDataSourceModel { - model := &customDomainDataSourceModel{ - ID: types.StringValue("test-project-id,test-distribution-id,https://testdomain.com"), - DistributionId: types.StringValue("test-distribution-id"), - ProjectId: types.StringValue("test-project-id"), - Name: types.StringValue("https://testdomain.com"), - Status: types.StringValue("ACTIVE"), - Errors: emtpyErrorsList, - Certificate: types.ObjectUnknown(certificateDataSourceTypes), - } - for _, mod := range mods { - mod(model) - } - return model - } - - // API response fixtures for custom and managed certificates - customType := "custom" - customVersion := int64(3) - getRespCustom := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{ - GetCustomDomainCustomCertificate: &cdn.GetCustomDomainCustomCertificate{ - Type: &customType, - Version: &customVersion, - }, - }) - - managedType := "managed" - getRespManaged := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{ - GetCustomDomainManagedCertificate: &cdn.GetCustomDomainManagedCertificate{ - Type: &managedType, - }, - }) - - // Helper to create API response fixtures - customDomainFixture := func(mods ...func(*cdn.GetCustomDomainResponse)) *cdn.GetCustomDomainResponse { - distribution := &cdn.CustomDomain{ - Errors: &[]cdn.StatusError{}, - Name: cdn.PtrString("https://testdomain.com"), - Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(), - } - customDomainResponse := &cdn.GetCustomDomainResponse{ - CustomDomain: distribution, - Certificate: getRespCustom, - } - - for _, mod := range mods { - mod(customDomainResponse) - } - return customDomainResponse - } - - // Test cases - tests := map[string]struct { - Input *cdn.GetCustomDomainResponse - Expected *customDomainDataSourceModel - IsValid bool - }{ - "happy_path_custom_cert": { - Expected: expectedModel(func(m *customDomainDataSourceModel) { - m.Certificate = certificateObj - }), - Input: customDomainFixture(), - IsValid: true, - }, - "happy_path_managed_cert": { - Expected: expectedModel(func(m *customDomainDataSourceModel) { - m.Certificate = types.ObjectNull(certificateDataSourceTypes) - }), - Input: customDomainFixture(func(gcdr *cdn.GetCustomDomainResponse) { - gcdr.Certificate = getRespManaged - }), - IsValid: true, - }, - "happy_path_status_error": { - Expected: expectedModel(func(m *customDomainDataSourceModel) { - m.Status = types.StringValue("ERROR") - m.Certificate = certificateObj - }), - Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) { - d.CustomDomain.Status = cdn.DOMAINSTATUS_ERROR.Ptr() - }), - IsValid: true, - }, - "sad_path_response_nil": { - Expected: expectedModel(), - Input: nil, - IsValid: false, - }, - "sad_path_name_missing": { - Expected: expectedModel(), - Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) { - d.CustomDomain.Name = nil - }), - IsValid: false, - }, - } - for tn, tc := range tests { - t.Run(tn, func(t *testing.T) { - model := &customDomainDataSourceModel{} - err := mapCustomDomainDataSourceFields(tc.Input, model, "test-project-id", "test-distribution-id") - - if err != nil && tc.IsValid { - t.Fatalf("Error mapping fields: %v", err) - } - if err == nil && !tc.IsValid { - t.Fatalf("Should have failed") - } - if tc.IsValid { - diff := cmp.Diff(tc.Expected, model) - if diff != "" { - t.Fatalf("Mapped model not as expected (-want +got):\n%s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/cdn/customdomain/resource.go b/stackit/internal/services/cdn/customdomain/resource.go deleted file mode 100644 index c031bd93..00000000 --- a/stackit/internal/services/cdn/customdomain/resource.go +++ /dev/null @@ -1,534 +0,0 @@ -package cdn - -import ( - "context" - "encoding/base64" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" - "github.com/stackitcloud/stackit-sdk-go/services/cdn/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &customDomainResource{} - _ resource.ResourceWithConfigure = &customDomainResource{} - _ resource.ResourceWithImportState = &customDomainResource{} -) -var certificateSchemaDescriptions = map[string]string{ - "main": "The TLS certificate for the custom domain. If omitted, a managed certificate will be used. If the block is specified, a custom certificate is used.", - "certificate": "The PEM-encoded TLS certificate. Required for custom certificates.", - "private_key": "The PEM-encoded private key for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.", - "version": "A version identifier for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.", -} - -var certificateTypes = map[string]attr.Type{ - "version": types.Int64Type, - "certificate": types.StringType, - "private_key": types.StringType, -} - -var customDomainSchemaDescriptions = map[string]string{ - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".", - "distribution_id": "CDN distribution ID", - "project_id": "STACKIT project ID associated with the distribution", - "status": "Status of the distribution", - "errors": "List of distribution errors", -} - -type CertificateModel struct { - Certificate types.String `tfsdk:"certificate"` - PrivateKey types.String `tfsdk:"private_key"` - Version types.Int64 `tfsdk:"version"` -} - -type CustomDomainModel struct { - ID types.String `tfsdk:"id"` // Required by Terraform - DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution - ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the cdn distribution - Name types.String `tfsdk:"name"` // The custom domain - Status types.String `tfsdk:"status"` // The status of the cdn distribution - Errors types.List `tfsdk:"errors"` // Any errors that the distribution has - Certificate types.Object `tfsdk:"certificate"` // the certificate of the custom domain -} - -type customDomainResource struct { - client *cdn.APIClient -} - -func NewCustomDomainResource() resource.Resource { - return &customDomainResource{} -} - -type Certificate struct { - Type string - Version *int64 -} - -func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "CDN client configured") -} - -func (r *customDomainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain" -} - -func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource), - Description: "CDN distribution data source schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["id"], - Computed: true, - }, - "name": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["name"], - Required: true, - Optional: false, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "distribution_id": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["distribution_id"], - Required: true, - Optional: false, - Validators: []validator.String{validate.UUID()}, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "project_id": schema.StringAttribute{ - Description: customDomainSchemaDescriptions["project_id"], - Required: true, - Optional: false, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "certificate": schema.SingleNestedAttribute{ - Description: certificateSchemaDescriptions["main"], - Optional: true, - Attributes: map[string]schema.Attribute{ - "certificate": schema.StringAttribute{ - Description: certificateSchemaDescriptions["certificate"], - Optional: true, - Sensitive: true, - }, - "private_key": schema.StringAttribute{ - Description: certificateSchemaDescriptions["private_key"], - Optional: true, - Sensitive: true, - }, - "version": schema.Int64Attribute{ - Description: certificateSchemaDescriptions["version"], - Computed: true, - }, - }, - }, - "status": schema.StringAttribute{ - Computed: true, - Description: customDomainSchemaDescriptions["status"], - }, - "errors": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - Description: customDomainSchemaDescriptions["errors"], - }, - }, - } -} - -func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model CustomDomainModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - name := model.Name.ValueString() - ctx = tflog.SetField(ctx, "name", name) - certificate, err := toCertificatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - payload := cdn.PutCustomDomainPayload{ - IntentId: cdn.PtrString(uuid.NewString()), - Certificate: certificate, - } - _, err = r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Waiting for create: %v", err)) - return - } - - respCustomDomain, err := r.client.GetCustomDomainExecute(ctx, projectId, distributionId, name) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err)) - return - } - err = mapCustomDomainResourceFields(respCustomDomain, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "CDN custom domain created") -} - -func (r *customDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model CustomDomainModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - name := model.Name.ValueString() - ctx = tflog.SetField(ctx, "name", name) - - customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - // n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapCustomDomainResourceFields(customDomainResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "CDN custom domain read") -} - -func (r *customDomainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model CustomDomainModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - name := model.Name.ValueString() - ctx = tflog.SetField(ctx, "name", name) - - certificate, err := toCertificatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - payload := cdn.PutCustomDomainPayload{ - IntentId: cdn.PtrString(uuid.NewString()), - Certificate: certificate, - } - _, err = r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Waiting for update: %v", err)) - return - } - - respCustomDomain, err := r.client.GetCustomDomainExecute(ctx, projectId, distributionId, name) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Calling API to read final state: %v", err)) - return - } - err = mapCustomDomainResourceFields(respCustomDomain, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "CDN custom domain certificate updated") -} - -func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model CustomDomainModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - name := model.Name.ValueString() - ctx = tflog.SetField(ctx, "name", name) - - _, err := r.client.DeleteCustomDomain(ctx, projectId, distributionId, name).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Delete custom domain: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Waiting for deletion: %v", err)) - return - } - tflog.Info(ctx, "CDN custom domain deleted") -} - -func (r *customDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN custom domain", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id]%q[custom_domain_name], got %q", core.Separator, core.Separator, req.ID)) - } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "CDN custom domain state imported") -} - -func normalizeCertificate(certInput cdn.GetCustomDomainResponseGetCertificateAttributeType) (Certificate, error) { - var customCert *cdn.GetCustomDomainCustomCertificate - var managedCert *cdn.GetCustomDomainManagedCertificate - - if certInput == nil { - return Certificate{}, errors.New("input of type GetCustomDomainResponseCertificate is nil") - } - customCert = certInput.GetCustomDomainCustomCertificate - managedCert = certInput.GetCustomDomainManagedCertificate - - // Now we process the extracted certificates - if customCert != nil && customCert.Type != nil && customCert.Version != nil { - return Certificate{ - Type: *customCert.Type, - Version: customCert.Version, // Converts from *int64 to int - }, nil - } - - if managedCert != nil && managedCert.Type != nil { - // The version will be the zero value for int (0), as requested - return Certificate{ - Type: *managedCert.Type, - }, nil - } - - return Certificate{}, errors.New("certificate structure is empty, neither custom nor managed is set") -} - -// toCertificatePayload constructs the certificate part of the payload for the API request. -// It defaults to a managed certificate if the certificate block is omitted, otherwise it creates a custom certificate. -func toCertificatePayload(ctx context.Context, model *CustomDomainModel) (*cdn.PutCustomDomainPayloadCertificate, error) { - // If the certificate block is not specified, default to a managed certificate. - if model.Certificate.IsNull() { - managedCert := cdn.NewPutCustomDomainManagedCertificate("managed") - certPayload := cdn.PutCustomDomainManagedCertificateAsPutCustomDomainPayloadCertificate(managedCert) - return &certPayload, nil - } - - var certModel CertificateModel - // Unpack the Terraform object into the temporary struct. - respDiags := model.Certificate.As(ctx, &certModel, basetypes.ObjectAsOptions{}) - if respDiags.HasError() { - return nil, fmt.Errorf("invalid certificate or private key: %w", core.DiagsToError(respDiags)) - } - - if utils.IsUndefined(certModel.Certificate) || utils.IsUndefined(certModel.PrivateKey) { - return nil, fmt.Errorf(`"certificate" and "private_key" must be set`) - } - - certStr := base64.StdEncoding.EncodeToString([]byte(certModel.Certificate.ValueString())) - keyStr := base64.StdEncoding.EncodeToString([]byte(certModel.PrivateKey.ValueString())) - - if certStr == "" || keyStr == "" { - return nil, errors.New("invalid certificate or private key. Please check if the string of the public certificate and private key in PEM format") - } - - customCert := cdn.NewPutCustomDomainCustomCertificate( - certStr, - keyStr, - "custom", - ) - certPayload := cdn.PutCustomDomainCustomCertificateAsPutCustomDomainPayloadCertificate(customCert) - - return &certPayload, nil -} - -func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResponse, model *CustomDomainModel) error { - if customDomainResponse == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if customDomainResponse.CustomDomain == nil { - return fmt.Errorf("CustomDomain is missing in response") - } - if customDomainResponse.CustomDomain.Name == nil { - return fmt.Errorf("name is missing in response") - } - - if customDomainResponse.CustomDomain.Status == nil { - return fmt.Errorf("status missing in response") - } - normalizedCert, err := normalizeCertificate(customDomainResponse.Certificate) - if err != nil { - return fmt.Errorf("Certificate error in normalizer: %w", err) - } - - // If the certificate is managed, the certificate block in the state should be null. - if normalizedCert.Type == "managed" { - model.Certificate = types.ObjectNull(certificateTypes) - } else { - // If the certificate is custom, we need to preserve the user-configured - // certificate and private key from the plan/state, and only update the computed version. - certAttributes := map[string]attr.Value{ - "certificate": types.StringNull(), // Default to null - "private_key": types.StringNull(), // Default to null - "version": types.Int64Null(), - } - - // Get existing values from the model's certificate object if it exists - if !model.Certificate.IsNull() { - existingAttrs := model.Certificate.Attributes() - if val, ok := existingAttrs["certificate"]; ok { - certAttributes["certificate"] = val - } - if val, ok := existingAttrs["private_key"]; ok { - certAttributes["private_key"] = val - } - } - - // Set the computed version from the API response - if normalizedCert.Version != nil { - certAttributes["version"] = types.Int64Value(*normalizedCert.Version) - } - - certificateObj, diags := types.ObjectValue(certificateTypes, certAttributes) - if diags.HasError() { - return fmt.Errorf("failed to map certificate: %w", core.DiagsToError(diags)) - } - model.Certificate = certificateObj - } - - model.ID = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.DistributionId.ValueString(), *customDomainResponse.CustomDomain.Name) - model.Status = types.StringValue(string(*customDomainResponse.CustomDomain.Status)) - - customDomainErrors := []attr.Value{} - if customDomainResponse.CustomDomain.Errors != nil { - for _, e := range *customDomainResponse.CustomDomain.Errors { - if e.En == nil { - return fmt.Errorf("error description missing") - } - customDomainErrors = append(customDomainErrors, types.StringValue(*e.En)) - } - } - modelErrors, diags := types.ListValue(types.StringType, customDomainErrors) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.Errors = modelErrors - - return nil -} diff --git a/stackit/internal/services/cdn/customdomain/resource_test.go b/stackit/internal/services/cdn/customdomain/resource_test.go deleted file mode 100644 index 28aff294..00000000 --- a/stackit/internal/services/cdn/customdomain/resource_test.go +++ /dev/null @@ -1,308 +0,0 @@ -package cdn - -import ( - "context" - cryptoRand "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/base64" - "encoding/pem" - "fmt" - "math/big" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" -) - -func TestMapFields(t *testing.T) { - // Redefine certificateTypes locally for testing, matching the updated schema - certificateTypes := map[string]attr.Type{ - "version": types.Int64Type, - "certificate": types.StringType, - "private_key": types.StringType, - } - - const dummyCert = "dummy-cert-pem" - const dummyKey = "dummy-key-pem" - - emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{}) - - // Expected object when a custom certificate is returned - certAttributes := map[string]attr.Value{ - "version": types.Int64Value(3), - "certificate": types.StringValue(dummyCert), - "private_key": types.StringValue(dummyKey), - } - certificateObj, _ := types.ObjectValue(certificateTypes, certAttributes) - - expectedModel := func(mods ...func(*CustomDomainModel)) *CustomDomainModel { - model := &CustomDomainModel{ - ID: types.StringValue("test-project-id,test-distribution-id,https://testdomain.com"), - DistributionId: types.StringValue("test-distribution-id"), - ProjectId: types.StringValue("test-project-id"), - Status: types.StringValue("ACTIVE"), - Errors: emtpyErrorsList, - Certificate: types.ObjectUnknown(certificateTypes), - } - for _, mod := range mods { - mod(model) - } - return model - } - - customType := "custom" - customVersion := int64(3) - getRespCustom := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{ - GetCustomDomainCustomCertificate: &cdn.GetCustomDomainCustomCertificate{ - Type: &customType, - Version: &customVersion, - }, - }) - - managedType := "managed" - getRespManaged := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{ - GetCustomDomainManagedCertificate: &cdn.GetCustomDomainManagedCertificate{ - Type: &managedType, - }, - }) - - customDomainFixture := func(mods ...func(*cdn.GetCustomDomainResponse)) *cdn.GetCustomDomainResponse { - distribution := &cdn.CustomDomain{ - Errors: &[]cdn.StatusError{}, - Name: cdn.PtrString("https://testdomain.com"), - Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(), - } - customDomainResponse := &cdn.GetCustomDomainResponse{ - CustomDomain: distribution, - Certificate: getRespCustom, - } - - for _, mod := range mods { - mod(customDomainResponse) - } - return customDomainResponse - } - - tests := map[string]struct { - Input *cdn.GetCustomDomainResponse - Certificate interface{} - Expected *CustomDomainModel - InitialModel *CustomDomainModel - IsValid bool - SkipInitialNil bool - }{ - "happy_path_custom_cert": { - Expected: expectedModel(func(m *CustomDomainModel) { - m.Certificate = certificateObj - }), - Input: customDomainFixture(), - IsValid: true, - InitialModel: expectedModel(func(m *CustomDomainModel) { - m.Certificate = basetypes.NewObjectValueMust(certificateTypes, map[string]attr.Value{ - "certificate": types.StringValue(dummyCert), - "private_key": types.StringValue(dummyKey), - "version": types.Int64Null(), - }) - }), - }, - "happy_path_managed_cert": { - Expected: expectedModel(func(m *CustomDomainModel) { - m.Certificate = types.ObjectNull(certificateTypes) - }), - Input: customDomainFixture(func(gcdr *cdn.GetCustomDomainResponse) { - gcdr.Certificate = getRespManaged - }), - IsValid: true, - InitialModel: expectedModel(func(m *CustomDomainModel) { m.Certificate = types.ObjectNull(certificateTypes) }), - }, - "happy_path_status_error": { - Expected: expectedModel(func(m *CustomDomainModel) { - m.Status = types.StringValue("ERROR") - m.Certificate = certificateObj - }), - Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) { - d.CustomDomain.Status = cdn.DOMAINSTATUS_ERROR.Ptr() - }), - IsValid: true, - InitialModel: expectedModel(func(m *CustomDomainModel) { - m.Certificate = basetypes.NewObjectValueMust(certificateTypes, map[string]attr.Value{ - "certificate": types.StringValue(dummyCert), - "private_key": types.StringValue(dummyKey), - "version": types.Int64Null(), - }) - }), - }, - "sad_path_custom_domain_nil": { - Expected: expectedModel(), - Input: nil, - IsValid: false, - InitialModel: &CustomDomainModel{}, - }, - "sad_path_name_missing": { - Expected: expectedModel(), - Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) { - d.CustomDomain.Name = nil - }), - IsValid: false, - InitialModel: &CustomDomainModel{}, - }, - } - for tn, tc := range tests { - t.Run(tn, func(t *testing.T) { - model := tc.InitialModel - model.DistributionId = tc.Expected.DistributionId - model.ProjectId = tc.Expected.ProjectId - err := mapCustomDomainResourceFields(tc.Input, model) - if err != nil && tc.IsValid { - t.Fatalf("Error mapping fields: %v", err) - } - if err == nil && !tc.IsValid { - t.Fatalf("Should have failed") - } - if tc.IsValid { - diff := cmp.Diff(tc.Expected, model) - if diff != "" { - t.Fatalf("Mapped model not as expected (-want +got):\n%s", diff) - } - } - }) - } -} - -func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { - privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048) - if err != nil { - t.Fatalf("failed to generate key: %s", err.Error()) - } - template := x509.Certificate{ - SerialNumber: big.NewInt(1), - Issuer: pkix.Name{CommonName: organization}, - Subject: pkix.Name{ - Organization: []string{organization}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Hour), - - KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - cert, err = x509.CreateCertificate( - cryptoRand.Reader, - &template, - &template, - &privateKey.PublicKey, - privateKey, - ) - if err != nil { - t.Fatalf("failed to generate cert: %s", err.Error()) - } - - return pem.EncodeToMemory(&pem.Block{ - Type: "CERTIFICATE", - Bytes: cert, - }), pem.EncodeToMemory(&pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privateKey), - }) -} -func TestToCertificatePayload(t *testing.T) { - organization := fmt.Sprintf("organization-%s", uuid.NewString()) - cert, key := makeCertAndKey(t, organization) - certPEM := string(cert) - keyPEM := string(key) - certBase64 := base64.StdEncoding.EncodeToString(cert) - keyBase64 := base64.StdEncoding.EncodeToString(key) - - tests := map[string]struct { - model *CustomDomainModel - expectedPayload *cdn.PutCustomDomainPayloadCertificate - expectErr bool - expectedErrMsg string - }{ - "success_managed_when_certificate_block_is_nil": { - model: &CustomDomainModel{ - Certificate: types.ObjectNull(certificateTypes), - }, - expectedPayload: &cdn.PutCustomDomainPayloadCertificate{ - PutCustomDomainManagedCertificate: cdn.NewPutCustomDomainManagedCertificate("managed"), - }, - expectErr: false, - }, - "success_custom_certificate": { - model: &CustomDomainModel{ - Certificate: basetypes.NewObjectValueMust( - certificateTypes, - map[string]attr.Value{ - "version": types.Int64Null(), - "certificate": types.StringValue(certPEM), - "private_key": types.StringValue(keyPEM), - }, - ), - }, - expectedPayload: &cdn.PutCustomDomainPayloadCertificate{ - PutCustomDomainCustomCertificate: cdn.NewPutCustomDomainCustomCertificate(certBase64, keyBase64, "custom"), - }, - expectErr: false, - }, - "fail_custom_missing_cert_value": { - model: &CustomDomainModel{ - Certificate: basetypes.NewObjectValueMust( - certificateTypes, - map[string]attr.Value{ - "version": types.Int64Null(), - "certificate": types.StringValue(""), // Empty certificate - "private_key": types.StringValue(keyPEM), - }, - ), - }, - expectErr: true, - expectedErrMsg: "invalid certificate or private key. Please check if the string of the public certificate and private key in PEM format", - }, - - "success_managed_when_certificate_attributes_are_nil": { - model: &CustomDomainModel{ - Certificate: basetypes.NewObjectValueMust( - certificateTypes, - map[string]attr.Value{ - "version": types.Int64Null(), - "certificate": types.StringNull(), - "private_key": types.StringNull(), - }, - ), - }, - expectErr: true, - expectedErrMsg: `"certificate" and "private_key" must be set`, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - payload, err := toCertificatePayload(context.Background(), tt.model) - if tt.expectErr { - if err == nil { - t.Fatalf("expected err, but got none") - } - if err.Error() != tt.expectedErrMsg { - t.Fatalf("expected err '%s', got '%s'", tt.expectedErrMsg, err.Error()) - } - return // Test ends here for failing cases - } - - if err != nil { - t.Fatalf("did not expect err, but got: %s", err.Error()) - } - - if diff := cmp.Diff(tt.expectedPayload, payload); diff != "" { - t.Errorf("payload mismatch (-want +got):\n%s", diff) - } - }) - } -} diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go deleted file mode 100644 index ce3f749c..00000000 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ /dev/null @@ -1,214 +0,0 @@ -package cdn - -import ( - "context" - "fmt" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -type distributionDataSource struct { - client *cdn.APIClient -} - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &distributionDataSource{} -) - -func NewDistributionDataSource() datasource.DataSource { - return &distributionDataSource{} -} - -func (d *distributionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_distribution", "datasource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "Service Account client configured") -} - -func (r *distributionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_cdn_distribution" -} - -func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - backendOptions := []string{"http"} - resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Datasource), - Description: "CDN distribution data source schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: schemaDescriptions["id"], - Computed: true, - }, - "distribution_id": schema.StringAttribute{ - Description: schemaDescriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "project_id": schema.StringAttribute{ - Description: schemaDescriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "status": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["status"], - }, - "created_at": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["created_at"], - }, - "updated_at": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["updated_at"], - }, - "errors": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - Description: schemaDescriptions["errors"], - }, - "domains": schema.ListNestedAttribute{ - Computed: true, - Description: schemaDescriptions["domains"], - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["domain_name"], - }, - "status": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["domain_status"], - }, - "type": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["domain_type"], - }, - "errors": schema.ListAttribute{ - Computed: true, - Description: schemaDescriptions["domain_errors"], - ElementType: types.StringType, - }, - }, - }, - }, - "config": schema.SingleNestedAttribute{ - Computed: true, - Description: schemaDescriptions["config"], - Attributes: map[string]schema.Attribute{ - "backend": schema.SingleNestedAttribute{ - Computed: true, - Description: schemaDescriptions["config_backend"], - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["config_backend_type"] + utils.FormatPossibleValues(backendOptions...), - }, - "origin_url": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["config_backend_origin_url"], - }, - "origin_request_headers": schema.MapAttribute{ - Computed: true, - Description: schemaDescriptions["config_backend_origin_request_headers"], - ElementType: types.StringType, - }, - "geofencing": schema.MapAttribute{ - Description: "A map of URLs to a list of countries where content is allowed.", - Computed: true, - ElementType: types.ListType{ - ElemType: types.StringType, - }, - }, - }, - }, - "regions": schema.ListAttribute{ - Computed: true, - Description: schemaDescriptions["config_regions"], - ElementType: types.StringType, - }, - "blocked_countries": schema.ListAttribute{ - Optional: true, - Description: schemaDescriptions["config_blocked_countries"], - ElementType: types.StringType, - }, - "optimizer": schema.SingleNestedAttribute{ - Description: schemaDescriptions["config_optimizer"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Computed: true, - }, - }, - }, - }, - }, - }, - } -} - -func (r *distributionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - distributionId := model.DistributionId.ValueString() - distributionResp, err := r.client.GetDistributionExecute(ctx, projectId, distributionId) - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading CDN distribution", - fmt.Sprintf("Unable to access CDN distribution %q.", distributionId), - map[int]string{}, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, distributionResp.Distribution, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Error processing API response: %v", err)) - return - } - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) -} diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go deleted file mode 100644 index 4b24968f..00000000 --- a/stackit/internal/services/cdn/distribution/resource.go +++ /dev/null @@ -1,940 +0,0 @@ -package cdn - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" - "github.com/stackitcloud/stackit-sdk-go/services/cdn/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &distributionResource{} - _ resource.ResourceWithConfigure = &distributionResource{} - _ resource.ResourceWithImportState = &distributionResource{} -) - -var schemaDescriptions = map[string]string{ - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".", - "distribution_id": "CDN distribution ID", - "project_id": "STACKIT project ID associated with the distribution", - "status": "Status of the distribution", - "created_at": "Time when the distribution was created", - "updated_at": "Time when the distribution was last updated", - "errors": "List of distribution errors", - "domains": "List of configured domains for the distribution", - "config": "The distribution configuration", - "config_backend": "The configured backend for the distribution", - "config_regions": "The configured regions where content will be hosted", - "config_backend_type": "The configured backend type. ", - "config_optimizer": "Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience.", - "config_backend_origin_url": "The configured backend type for the distribution", - "config_backend_origin_request_headers": "The configured origin request headers for the backend", - "config_blocked_countries": "The configured countries where distribution of content is blocked", - "domain_name": "The name of the domain", - "domain_status": "The status of the domain", - "domain_type": "The type of the domain. Each distribution has one domain of type \"managed\", and domains of type \"custom\" may be additionally created by the user", - "domain_errors": "List of domain errors", -} - -type Model struct { - ID types.String `tfsdk:"id"` // Required by Terraform - DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution - ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the cdn distribution - Status types.String `tfsdk:"status"` // The status of the cdn distribution - CreatedAt types.String `tfsdk:"created_at"` // When the distribution was created - UpdatedAt types.String `tfsdk:"updated_at"` // When the distribution was last updated - Errors types.List `tfsdk:"errors"` // Any errors that the distribution has - Domains types.List `tfsdk:"domains"` // The domains associated with the distribution - Config types.Object `tfsdk:"config"` // the configuration of the distribution -} - -type distributionConfig struct { - Backend backend `tfsdk:"backend"` // The backend associated with the distribution - Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached - BlockedCountries *[]string `tfsdk:"blocked_countries"` // The countries for which content will be blocked - Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration -} - -type optimizerConfig struct { - Enabled types.Bool `tfsdk:"enabled"` -} - -type backend struct { - Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" backend is supported - OriginURL string `tfsdk:"origin_url"` // The origin URL of the backend - OriginRequestHeaders *map[string]string `tfsdk:"origin_request_headers"` // Request headers that should be added by the CDN distribution to incoming requests - Geofencing *map[string][]*string `tfsdk:"geofencing"` // The geofencing is an object mapping multiple alternative origins to country codes. -} - -var configTypes = map[string]attr.Type{ - "backend": types.ObjectType{AttrTypes: backendTypes}, - "regions": types.ListType{ElemType: types.StringType}, - "blocked_countries": types.ListType{ElemType: types.StringType}, - "optimizer": types.ObjectType{ - AttrTypes: optimizerTypes, - }, -} - -var optimizerTypes = map[string]attr.Type{ - "enabled": types.BoolType, -} - -var geofencingTypes = types.MapType{ElemType: types.ListType{ - ElemType: types.StringType, -}} - -var backendTypes = map[string]attr.Type{ - "type": types.StringType, - "origin_url": types.StringType, - "origin_request_headers": types.MapType{ElemType: types.StringType}, - "geofencing": geofencingTypes, -} - -var domainTypes = map[string]attr.Type{ - "name": types.StringType, - "status": types.StringType, - "type": types.StringType, - "errors": types.ListType{ElemType: types.StringType}, -} - -type distributionResource struct { - client *cdn.APIClient - providerData core.ProviderData -} - -func NewDistributionResource() resource.Resource { - return &distributionResource{} -} - -func (r *distributionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_distribution", "resource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := cdnUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "CDN client configured") -} - -func (r *distributionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_cdn_distribution" -} - -func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - backendOptions := []string{"http"} - resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource), - Description: "CDN distribution data source schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: schemaDescriptions["id"], - Computed: true, - }, - "distribution_id": schema.StringAttribute{ - Description: schemaDescriptions["distribution_id"], - Computed: true, - Validators: []validator.String{validate.UUID()}, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: schemaDescriptions["project_id"], - Required: true, - Optional: false, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "status": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["status"], - }, - "created_at": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["created_at"], - }, - "updated_at": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["updated_at"], - }, - "errors": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - Description: schemaDescriptions["errors"], - }, - "domains": schema.ListNestedAttribute{ - Computed: true, - Description: schemaDescriptions["domains"], - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["domain_name"], - }, - "status": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["domain_status"], - }, - "type": schema.StringAttribute{ - Computed: true, - Description: schemaDescriptions["domain_type"], - }, - "errors": schema.ListAttribute{ - Computed: true, - Description: schemaDescriptions["domain_errors"], - ElementType: types.StringType, - }, - }, - }, - }, - "config": schema.SingleNestedAttribute{ - Required: true, - Description: schemaDescriptions["config"], - Attributes: map[string]schema.Attribute{ - "optimizer": schema.SingleNestedAttribute{ - Description: schemaDescriptions["config_optimizer"], - Optional: true, - Computed: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Optional: true, - Computed: true, - }, - }, - Validators: []validator.Object{ - objectvalidator.AlsoRequires(path.MatchRelative().AtName("enabled")), - }, - }, - "backend": schema.SingleNestedAttribute{ - Required: true, - Description: schemaDescriptions["config_backend"], - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Required: true, - Description: schemaDescriptions["config_backend_type"] + utils.FormatPossibleValues(backendOptions...), - Validators: []validator.String{stringvalidator.OneOf(backendOptions...)}, - }, - "origin_url": schema.StringAttribute{ - Required: true, - Description: schemaDescriptions["config_backend_origin_url"], - }, - "origin_request_headers": schema.MapAttribute{ - Optional: true, - Description: schemaDescriptions["config_backend_origin_request_headers"], - ElementType: types.StringType, - }, - "geofencing": schema.MapAttribute{ - Description: "A map of URLs to a list of countries where content is allowed.", - Optional: true, - ElementType: types.ListType{ - ElemType: types.StringType, - }, - Validators: []validator.Map{ - mapvalidator.SizeAtLeast(1), - }, - }, - }, - }, - "regions": schema.ListAttribute{ - Required: true, - Description: schemaDescriptions["config_regions"], - ElementType: types.StringType, - }, - "blocked_countries": schema.ListAttribute{ - Optional: true, - Description: schemaDescriptions["config_blocked_countries"], - ElementType: types.StringType, - }, - }, - }, - }, - } -} - -func (r *distributionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var model Model - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - if !utils.IsUndefined(model.Config) { - var config distributionConfig - if !model.Config.IsNull() { - diags := model.Config.As(ctx, &config, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return - } - if geofencing := config.Backend.Geofencing; geofencing != nil { - for url, region := range *geofencing { - if region == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be null.", url)) - continue - } - if len(region) == 0 { - core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be empty.", url)) - continue - } - - for i, countryPtr := range region { - if countryPtr == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("Found a null value in the country list for URL %q at index %d.", url, i)) - break - } - } - } - } - } - } -} - -func (r *distributionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - createResp, err := r.client.CreateDistribution(ctx, projectId).CreateDistributionPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.CreateDistributionPoolWaitHandler(ctx, r.client, projectId, *createResp.Distribution.Id).SetTimeout(5 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Waiting for create: %v", err)) - return - } - - err = mapFields(ctx, waitResp.Distribution, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "CDN distribution created") -} - -func (r *distributionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - - cdnResp, err := r.client.GetDistribution(ctx, projectId, distributionId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - // n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, cdnResp.Distribution, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN ditribution", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "CDN distribution read") -} - -func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - - configModel := distributionConfig{} - diags = model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{ - UnhandledNullAsEmpty: false, - UnhandledUnknownAsEmpty: false, - }) - if diags.HasError() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping config") - return - } - - regions := []cdn.Region{} - for _, r := range *configModel.Regions { - regionEnum, err := cdn.NewRegionFromValue(r) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Map regions: %v", err)) - return - } - regions = append(regions, *regionEnum) - } - - // blockedCountries - // Use a pointer to a slice to distinguish between an empty list (unblock all) and nil (no change). - var blockedCountries *[]string - if configModel.BlockedCountries != nil { - // Use a temporary slice - tempBlockedCountries := []string{} - - for _, blockedCountry := range *configModel.BlockedCountries { - validatedBlockedCountry, err := validateCountryCode(blockedCountry) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Blocked countries: %v", err)) - return - } - tempBlockedCountries = append(tempBlockedCountries, validatedBlockedCountry) - } - - // Point to the populated slice - blockedCountries = &tempBlockedCountries - } - - geofencingPatch := map[string][]string{} - if configModel.Backend.Geofencing != nil { - gf := make(map[string][]string) - for url, countries := range *configModel.Backend.Geofencing { - countryStrings := make([]string, len(countries)) - for i, countryPtr := range countries { - if countryPtr == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Geofencing url %q has a null value", url)) - return - } - countryStrings[i] = *countryPtr - } - gf[url] = countryStrings - } - geofencingPatch = gf - } - - configPatch := &cdn.ConfigPatch{ - Backend: &cdn.ConfigPatchBackend{ - HttpBackendPatch: &cdn.HttpBackendPatch{ - OriginRequestHeaders: configModel.Backend.OriginRequestHeaders, - OriginUrl: &configModel.Backend.OriginURL, - Type: &configModel.Backend.Type, - Geofencing: &geofencingPatch, // Use the converted variable - }, - }, - Regions: ®ions, - BlockedCountries: blockedCountries, - } - - if !utils.IsUndefined(configModel.Optimizer) { - var optimizerModel optimizerConfig - - diags = configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping optimizer config") - return - } - - optimizer := cdn.NewOptimizerPatch() - if !utils.IsUndefined(optimizerModel.Enabled) { - optimizer.SetEnabled(optimizerModel.Enabled.ValueBool()) - } - configPatch.Optimizer = optimizer - } - - _, err := r.client.PatchDistribution(ctx, projectId, distributionId).PatchDistributionPayload(cdn.PatchDistributionPayload{ - Config: configPatch, - IntentId: cdn.PtrString(uuid.NewString()), - }).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Patch distribution: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.UpdateDistributionWaitHandler(ctx, r.client, projectId, distributionId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Waiting for update: %v", err)) - return - } - - err = mapFields(ctx, waitResp.Distribution, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "CDN distribution updated") -} - -func (r *distributionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.LogResponse(ctx) - - projectId := model.ProjectId.ValueString() - distributionId := model.DistributionId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "distribution_id", distributionId) - - _, err := r.client.DeleteDistribution(ctx, projectId, distributionId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN distribution", fmt.Sprintf("Delete distribution: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteDistributionWaitHandler(ctx, r.client, projectId, distributionId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN distribution", fmt.Sprintf("Waiting for deletion: %v", err)) - return - } - tflog.Info(ctx, "CDN distribution deleted") -} - -func (r *distributionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN distribution", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id], got %q", core.Separator, req.ID)) - } - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...) - tflog.Info(ctx, "CDN distribution state imported") -} - -func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model) error { - if distribution == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if distribution.ProjectId == nil { - return fmt.Errorf("Project ID not present") - } - - if distribution.Id == nil { - return fmt.Errorf("CDN distribution ID not present") - } - - if distribution.CreatedAt == nil { - return fmt.Errorf("CreatedAt missing in response") - } - - if distribution.UpdatedAt == nil { - return fmt.Errorf("UpdatedAt missing in response") - } - - if distribution.Status == nil { - return fmt.Errorf("Status missing in response") - } - - model.ID = utils.BuildInternalTerraformId(*distribution.ProjectId, *distribution.Id) - model.DistributionId = types.StringValue(*distribution.Id) - model.ProjectId = types.StringValue(*distribution.ProjectId) - model.Status = types.StringValue(string(distribution.GetStatus())) - model.CreatedAt = types.StringValue(distribution.CreatedAt.String()) - model.UpdatedAt = types.StringValue(distribution.UpdatedAt.String()) - - // distributionErrors - distributionErrors := []attr.Value{} - if distribution.Errors != nil { - for _, e := range *distribution.Errors { - distributionErrors = append(distributionErrors, types.StringValue(*e.En)) - } - } - modelErrors, diags := types.ListValue(types.StringType, distributionErrors) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.Errors = modelErrors - - // regions - regions := []attr.Value{} - for _, r := range *distribution.Config.Regions { - regions = append(regions, types.StringValue(string(r))) - } - modelRegions, diags := types.ListValue(types.StringType, regions) - if diags.HasError() { - return core.DiagsToError(diags) - } - - // blockedCountries - var blockedCountries []attr.Value - if distribution.Config != nil && distribution.Config.BlockedCountries != nil { - for _, c := range *distribution.Config.BlockedCountries { - blockedCountries = append(blockedCountries, types.StringValue(string(c))) - } - } - - modelBlockedCountries, diags := types.ListValue(types.StringType, blockedCountries) - if diags.HasError() { - return core.DiagsToError(diags) - } - - // originRequestHeaders - originRequestHeaders := types.MapNull(types.StringType) - if origHeaders := distribution.Config.Backend.HttpBackend.OriginRequestHeaders; origHeaders != nil && len(*origHeaders) > 0 { - headers := map[string]attr.Value{} - for k, v := range *origHeaders { - headers[k] = types.StringValue(v) - } - mappedHeaders, diags := types.MapValue(types.StringType, headers) - originRequestHeaders = mappedHeaders - if diags.HasError() { - return core.DiagsToError(diags) - } - } - - // geofencing - var oldConfig distributionConfig - oldGeofencingMap := make(map[string][]*string) - if !model.Config.IsNull() { - diags = model.Config.As(ctx, &oldConfig, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return core.DiagsToError(diags) - } - if oldConfig.Backend.Geofencing != nil { - oldGeofencingMap = *oldConfig.Backend.Geofencing - } - } - - reconciledGeofencingData := make(map[string][]string) - if geofencingAPI := distribution.Config.Backend.HttpBackend.Geofencing; geofencingAPI != nil && len(*geofencingAPI) > 0 { - newGeofencingMap := *geofencingAPI - for url, newCountries := range newGeofencingMap { - oldCountriesPtrs := oldGeofencingMap[url] - - oldCountries := utils.ConvertPointerSliceToStringSlice(oldCountriesPtrs) - - reconciledCountries := utils.ReconcileStringSlices(oldCountries, newCountries) - reconciledGeofencingData[url] = reconciledCountries - } - } - - geofencingVal := types.MapNull(geofencingTypes.ElemType) - if len(reconciledGeofencingData) > 0 { - geofencingMapElems := make(map[string]attr.Value) - for url, countries := range reconciledGeofencingData { - listVal, diags := types.ListValueFrom(ctx, types.StringType, countries) - if diags.HasError() { - return core.DiagsToError(diags) - } - geofencingMapElems[url] = listVal - } - - var mappedGeofencing basetypes.MapValue - mappedGeofencing, diags = types.MapValue(geofencingTypes.ElemType, geofencingMapElems) - if diags.HasError() { - return core.DiagsToError(diags) - } - geofencingVal = mappedGeofencing - } - - // note that httpbackend is hardcoded here as long as it is the only available backend - backend, diags := types.ObjectValue(backendTypes, map[string]attr.Value{ - "type": types.StringValue(*distribution.Config.Backend.HttpBackend.Type), - "origin_url": types.StringValue(*distribution.Config.Backend.HttpBackend.OriginUrl), - "origin_request_headers": originRequestHeaders, - "geofencing": geofencingVal, - }) - if diags.HasError() { - return core.DiagsToError(diags) - } - - optimizerVal := types.ObjectNull(optimizerTypes) - if o := distribution.Config.Optimizer; o != nil { - optimizerEnabled, ok := o.GetEnabledOk() - if ok { - var diags diag.Diagnostics - optimizerVal, diags = types.ObjectValue(optimizerTypes, map[string]attr.Value{ - "enabled": types.BoolValue(optimizerEnabled), - }) - if diags.HasError() { - return core.DiagsToError(diags) - } - } - } - cfg, diags := types.ObjectValue(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": modelRegions, - "blocked_countries": modelBlockedCountries, - "optimizer": optimizerVal, - }) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.Config = cfg - - domains := []attr.Value{} - if distribution.Domains != nil { - for _, d := range *distribution.Domains { - domainErrors := []attr.Value{} - if d.Errors != nil { - for _, e := range *d.Errors { - if e.En == nil { - return fmt.Errorf("error description missing") - } - domainErrors = append(domainErrors, types.StringValue(*e.En)) - } - } - modelDomainErrors, diags := types.ListValue(types.StringType, domainErrors) - if diags.HasError() { - return core.DiagsToError(diags) - } - if d.Name == nil || d.Status == nil || d.Type == nil { - return fmt.Errorf("domain entry incomplete") - } - modelDomain, diags := types.ObjectValue(domainTypes, map[string]attr.Value{ - "name": types.StringValue(*d.Name), - "status": types.StringValue(string(*d.Status)), - "type": types.StringValue(string(*d.Type)), - "errors": modelDomainErrors, - }) - if diags.HasError() { - return core.DiagsToError(diags) - } - - domains = append(domains, modelDomain) - } - } - - modelDomains, diags := types.ListValue(types.ObjectType{AttrTypes: domainTypes}, domains) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.Domains = modelDomains - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistributionPayload, error) { - if model == nil { - return nil, fmt.Errorf("missing model") - } - cfg, err := convertConfig(ctx, model) - if err != nil { - return nil, err - } - var optimizer *cdn.Optimizer - if cfg.Optimizer != nil { - optimizer = cdn.NewOptimizer(cfg.Optimizer.GetEnabled()) - } - - payload := &cdn.CreateDistributionPayload{ - IntentId: cdn.PtrString(uuid.NewString()), - OriginUrl: cfg.Backend.HttpBackend.OriginUrl, - Regions: cfg.Regions, - BlockedCountries: cfg.BlockedCountries, - OriginRequestHeaders: cfg.Backend.HttpBackend.OriginRequestHeaders, - Geofencing: cfg.Backend.HttpBackend.Geofencing, - Optimizer: optimizer, - } - - return payload, nil -} - -func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { - if model == nil { - return nil, errors.New("model cannot be nil") - } - if model.Config.IsNull() || model.Config.IsUnknown() { - return nil, errors.New("config cannot be nil or unknown") - } - configModel := distributionConfig{} - diags := model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{ - UnhandledNullAsEmpty: false, - UnhandledUnknownAsEmpty: false, - }) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - // regions - regions := []cdn.Region{} - for _, r := range *configModel.Regions { - regionEnum, err := cdn.NewRegionFromValue(r) - if err != nil { - return nil, err - } - regions = append(regions, *regionEnum) - } - - // blockedCountries - var blockedCountries []string - if configModel.BlockedCountries != nil { - for _, blockedCountry := range *configModel.BlockedCountries { - validatedBlockedCountry, err := validateCountryCode(blockedCountry) - if err != nil { - return nil, err - } - blockedCountries = append(blockedCountries, validatedBlockedCountry) - } - } - - // geofencing - geofencing := map[string][]string{} - if configModel.Backend.Geofencing != nil { - for endpoint, countryCodes := range *configModel.Backend.Geofencing { - geofencingCountry := make([]string, len(countryCodes)) - for i, countryCodePtr := range countryCodes { - if countryCodePtr == nil { - return nil, fmt.Errorf("geofencing url %q has a null value", endpoint) - } - validatedCountry, err := validateCountryCode(*countryCodePtr) - if err != nil { - return nil, err - } - geofencingCountry[i] = validatedCountry - } - geofencing[endpoint] = geofencingCountry - } - } - - // originRequestHeaders - originRequestHeaders := map[string]string{} - if configModel.Backend.OriginRequestHeaders != nil { - for k, v := range *configModel.Backend.OriginRequestHeaders { - originRequestHeaders[k] = v - } - } - - cdnConfig := &cdn.Config{ - Backend: &cdn.ConfigBackend{ - HttpBackend: &cdn.HttpBackend{ - OriginRequestHeaders: &originRequestHeaders, - OriginUrl: &configModel.Backend.OriginURL, - Type: &configModel.Backend.Type, - Geofencing: &geofencing, - }, - }, - Regions: ®ions, - BlockedCountries: &blockedCountries, - } - - if !utils.IsUndefined(configModel.Optimizer) { - var optimizerModel optimizerConfig - diags := configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - if !utils.IsUndefined(optimizerModel.Enabled) { - cdnConfig.Optimizer = cdn.NewOptimizer(optimizerModel.Enabled.ValueBool()) - } - } - - return cdnConfig, nil -} - -// validateCountryCode checks for a valid country user input. This is just a quick check -// since the API already does a more thorough check. -func validateCountryCode(country string) (string, error) { - if len(country) != 2 { - return "", errors.New("country code must be exactly 2 characters long") - } - - upperCountry := strings.ToUpper(country) - - // Check if both characters are alphabetical letters within the ASCII range A-Z. - // Yes, we could use the unicode package, but we are only targeting ASCII letters specifically, so - // let's omit this dependency. - char1 := upperCountry[0] - char2 := upperCountry[1] - - if !((char1 >= 'A' && char1 <= 'Z') && (char2 >= 'A' && char2 <= 'Z')) { - return "", fmt.Errorf("country code '%s' must consist of two alphabetical letters (A-Z or a-z)", country) - } - - return upperCountry, nil -} diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go deleted file mode 100644 index 1a639436..00000000 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ /dev/null @@ -1,589 +0,0 @@ -package cdn - -import ( - "context" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" -) - -func TestToCreatePayload(t *testing.T) { - headers := map[string]attr.Value{ - "testHeader0": types.StringValue("testHeaderValue0"), - "testHeader1": types.StringValue("testHeaderValue1"), - } - originRequestHeaders := types.MapValueMust(types.StringType, headers) - geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("DE"), - types.StringValue("FR"), - }) - geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{ - "https://de.mycoolapp.com": geofencingCountries, - }) - backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{ - "type": types.StringValue("http"), - "origin_url": types.StringValue("https://www.mycoolapp.com"), - "origin_request_headers": originRequestHeaders, - "geofencing": geofencing, - }) - regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} - regionsFixture := types.ListValueMust(types.StringType, regions) - blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")} - blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) - optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - }) - config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "blocked_countries": blockedCountriesFixture, - "optimizer": types.ObjectNull(optimizerTypes), - }) - modelFixture := func(mods ...func(*Model)) *Model { - model := &Model{ - DistributionId: types.StringValue("test-distribution-id"), - ProjectId: types.StringValue("test-project-id"), - Config: config, - } - for _, mod := range mods { - mod(model) - } - return model - } - tests := map[string]struct { - Input *Model - Expected *cdn.CreateDistributionPayload - IsValid bool - }{ - "happy_path": { - Input: modelFixture(), - Expected: &cdn.CreateDistributionPayload{ - OriginRequestHeaders: &map[string]string{ - "testHeader0": "testHeaderValue0", - "testHeader1": "testHeaderValue1", - }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Regions: &[]cdn.Region{"EU", "US"}, - BlockedCountries: &[]string{"XX", "YY", "ZZ"}, - Geofencing: &map[string][]string{ - "https://de.mycoolapp.com": {"DE", "FR"}, - }, - }, - IsValid: true, - }, - "happy_path_with_optimizer": { - Input: modelFixture(func(m *Model) { - m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": optimizer, - "blocked_countries": blockedCountriesFixture, - }) - }), - Expected: &cdn.CreateDistributionPayload{ - OriginRequestHeaders: &map[string]string{ - "testHeader0": "testHeaderValue0", - "testHeader1": "testHeaderValue1", - }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Regions: &[]cdn.Region{"EU", "US"}, - Optimizer: cdn.NewOptimizer(true), - BlockedCountries: &[]string{"XX", "YY", "ZZ"}, - Geofencing: &map[string][]string{ - "https://de.mycoolapp.com": {"DE", "FR"}, - }, - }, - IsValid: true, - }, - "sad_path_model_nil": { - Input: nil, - Expected: nil, - IsValid: false, - }, - "sad_path_config_error": { - Input: modelFixture(func(m *Model) { - m.Config = types.ObjectNull(configTypes) - }), - Expected: nil, - IsValid: false, - }, - } - for tn, tc := range tests { - t.Run(tn, func(t *testing.T) { - res, err := toCreatePayload(context.Background(), tc.Input) - if err != nil && tc.IsValid { - t.Fatalf("Error converting model to create payload: %v", err) - } - if err == nil && !tc.IsValid { - t.Fatalf("Should have failed") - } - if tc.IsValid { - // set generated ID before diffing - tc.Expected.IntentId = res.IntentId - - diff := cmp.Diff(res, tc.Expected) - if diff != "" { - t.Fatalf("Create Payload not as expected: %s", diff) - } - } - }) - } -} - -func TestConvertConfig(t *testing.T) { - headers := map[string]attr.Value{ - "testHeader0": types.StringValue("testHeaderValue0"), - "testHeader1": types.StringValue("testHeaderValue1"), - } - originRequestHeaders := types.MapValueMust(types.StringType, headers) - geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("DE"), - types.StringValue("FR"), - }) - geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{ - "https://de.mycoolapp.com": geofencingCountries, - }) - backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{ - "type": types.StringValue("http"), - "origin_url": types.StringValue("https://www.mycoolapp.com"), - "origin_request_headers": originRequestHeaders, - "geofencing": geofencing, - }) - regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} - regionsFixture := types.ListValueMust(types.StringType, regions) - blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")} - blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) - optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{"enabled": types.BoolValue(true)}) - config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": types.ObjectNull(optimizerTypes), - "blocked_countries": blockedCountriesFixture, - }) - modelFixture := func(mods ...func(*Model)) *Model { - model := &Model{ - DistributionId: types.StringValue("test-distribution-id"), - ProjectId: types.StringValue("test-project-id"), - Config: config, - } - for _, mod := range mods { - mod(model) - } - return model - } - tests := map[string]struct { - Input *Model - Expected *cdn.Config - IsValid bool - }{ - "happy_path": { - Input: modelFixture(), - Expected: &cdn.Config{ - Backend: &cdn.ConfigBackend{ - HttpBackend: &cdn.HttpBackend{ - OriginRequestHeaders: &map[string]string{ - "testHeader0": "testHeaderValue0", - "testHeader1": "testHeaderValue1", - }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Type: cdn.PtrString("http"), - Geofencing: &map[string][]string{ - "https://de.mycoolapp.com": {"DE", "FR"}, - }, - }, - }, - Regions: &[]cdn.Region{"EU", "US"}, - BlockedCountries: &[]string{"XX", "YY", "ZZ"}, - }, - IsValid: true, - }, - "happy_path_with_optimizer": { - Input: modelFixture(func(m *Model) { - m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": optimizer, - "blocked_countries": blockedCountriesFixture, - }) - }), - Expected: &cdn.Config{ - Backend: &cdn.ConfigBackend{ - HttpBackend: &cdn.HttpBackend{ - OriginRequestHeaders: &map[string]string{ - "testHeader0": "testHeaderValue0", - "testHeader1": "testHeaderValue1", - }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Type: cdn.PtrString("http"), - Geofencing: &map[string][]string{ - "https://de.mycoolapp.com": {"DE", "FR"}, - }, - }, - }, - Regions: &[]cdn.Region{"EU", "US"}, - Optimizer: cdn.NewOptimizer(true), - BlockedCountries: &[]string{"XX", "YY", "ZZ"}, - }, - IsValid: true, - }, - "sad_path_model_nil": { - Input: nil, - Expected: nil, - IsValid: false, - }, - "sad_path_config_error": { - Input: modelFixture(func(m *Model) { - m.Config = types.ObjectNull(configTypes) - }), - Expected: nil, - IsValid: false, - }, - } - for tn, tc := range tests { - t.Run(tn, func(t *testing.T) { - res, err := convertConfig(context.Background(), tc.Input) - if err != nil && tc.IsValid { - t.Fatalf("Error converting model to create payload: %v", err) - } - if err == nil && !tc.IsValid { - t.Fatalf("Should have failed") - } - if tc.IsValid { - diff := cmp.Diff(res, tc.Expected) - if diff != "" { - t.Fatalf("Create Payload not as expected: %s", diff) - } - } - }) - } -} - -func TestMapFields(t *testing.T) { - createdAt := time.Now() - updatedAt := time.Now() - headers := map[string]attr.Value{ - "testHeader0": types.StringValue("testHeaderValue0"), - "testHeader1": types.StringValue("testHeaderValue1"), - } - originRequestHeaders := types.MapValueMust(types.StringType, headers) - backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{ - "type": types.StringValue("http"), - "origin_url": types.StringValue("https://www.mycoolapp.com"), - "origin_request_headers": originRequestHeaders, - "geofencing": types.MapNull(geofencingTypes.ElemType), - }) - regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")} - regionsFixture := types.ListValueMust(types.StringType, regions) - blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")} - blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries) - geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("DE"), types.StringValue("BR")}) - geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{ - "test/": geofencingCountries, - }) - geofencingInput := map[string][]string{"test/": {"DE", "BR"}} - optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - }) - config := types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "blocked_countries": blockedCountriesFixture, - "optimizer": types.ObjectNull(optimizerTypes), - }) - - emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{}) - managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{ - "name": types.StringValue("test.stackit-cdn.com"), - "status": types.StringValue("ACTIVE"), - "type": types.StringValue("managed"), - "errors": types.ListValueMust(types.StringType, []attr.Value{}), - }) - domains := types.ListValueMust(types.ObjectType{AttrTypes: domainTypes}, []attr.Value{managedDomain}) - expectedModel := func(mods ...func(*Model)) *Model { - model := &Model{ - ID: types.StringValue("test-project-id,test-distribution-id"), - DistributionId: types.StringValue("test-distribution-id"), - ProjectId: types.StringValue("test-project-id"), - Config: config, - Status: types.StringValue("ACTIVE"), - CreatedAt: types.StringValue(createdAt.String()), - UpdatedAt: types.StringValue(updatedAt.String()), - Errors: emtpyErrorsList, - Domains: domains, - } - for _, mod := range mods { - mod(model) - } - return model - } - distributionFixture := func(mods ...func(*cdn.Distribution)) *cdn.Distribution { - distribution := &cdn.Distribution{ - Config: &cdn.Config{ - Backend: &cdn.ConfigBackend{ - HttpBackend: &cdn.HttpBackend{ - OriginRequestHeaders: &map[string]string{ - "testHeader0": "testHeaderValue0", - "testHeader1": "testHeaderValue1", - }, - OriginUrl: cdn.PtrString("https://www.mycoolapp.com"), - Type: cdn.PtrString("http"), - }, - }, - Regions: &[]cdn.Region{"EU", "US"}, - BlockedCountries: &[]string{"XX", "YY", "ZZ"}, - Optimizer: nil, - }, - CreatedAt: &createdAt, - Domains: &[]cdn.Domain{ - { - Name: cdn.PtrString("test.stackit-cdn.com"), - Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(), - Type: cdn.DOMAINTYPE_MANAGED.Ptr(), - }, - }, - Id: cdn.PtrString("test-distribution-id"), - ProjectId: cdn.PtrString("test-project-id"), - Status: cdn.DISTRIBUTIONSTATUS_ACTIVE.Ptr(), - UpdatedAt: &updatedAt, - } - for _, mod := range mods { - mod(distribution) - } - return distribution - } - tests := map[string]struct { - Input *cdn.Distribution - Expected *Model - IsValid bool - }{ - "happy_path": { - Expected: expectedModel(), - Input: distributionFixture(), - IsValid: true, - }, - "happy_path_with_optimizer": { - Expected: expectedModel(func(m *Model) { - m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backend, - "regions": regionsFixture, - "optimizer": optimizer, - "blocked_countries": blockedCountriesFixture, - }) - }), - Input: distributionFixture(func(d *cdn.Distribution) { - d.Config.Optimizer = &cdn.Optimizer{ - Enabled: cdn.PtrBool(true), - } - }), - IsValid: true, - }, - "happy_path_with_geofencing": { - Expected: expectedModel(func(m *Model) { - backendWithGeofencing := types.ObjectValueMust(backendTypes, map[string]attr.Value{ - "type": types.StringValue("http"), - "origin_url": types.StringValue("https://www.mycoolapp.com"), - "origin_request_headers": originRequestHeaders, - "geofencing": geofencing, - }) - m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{ - "backend": backendWithGeofencing, - "regions": regionsFixture, - "optimizer": types.ObjectNull(optimizerTypes), - "blocked_countries": blockedCountriesFixture, - }) - }), - Input: distributionFixture(func(d *cdn.Distribution) { - d.Config.Backend.HttpBackend.Geofencing = &geofencingInput - }), - IsValid: true, - }, - "happy_path_status_error": { - Expected: expectedModel(func(m *Model) { - m.Status = types.StringValue("ERROR") - }), - Input: distributionFixture(func(d *cdn.Distribution) { - d.Status = cdn.DISTRIBUTIONSTATUS_ERROR.Ptr() - }), - IsValid: true, - }, - "happy_path_custom_domain": { - Expected: expectedModel(func(m *Model) { - managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{ - "name": types.StringValue("test.stackit-cdn.com"), - "status": types.StringValue("ACTIVE"), - "type": types.StringValue("managed"), - "errors": types.ListValueMust(types.StringType, []attr.Value{}), - }) - customDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{ - "name": types.StringValue("mycoolapp.info"), - "status": types.StringValue("ACTIVE"), - "type": types.StringValue("custom"), - "errors": types.ListValueMust(types.StringType, []attr.Value{}), - }) - domains := types.ListValueMust(types.ObjectType{AttrTypes: domainTypes}, []attr.Value{managedDomain, customDomain}) - m.Domains = domains - }), - Input: distributionFixture(func(d *cdn.Distribution) { - d.Domains = &[]cdn.Domain{ - { - Name: cdn.PtrString("test.stackit-cdn.com"), - Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(), - Type: cdn.DOMAINTYPE_MANAGED.Ptr(), - }, - { - Name: cdn.PtrString("mycoolapp.info"), - Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(), - Type: cdn.DOMAINTYPE_CUSTOM.Ptr(), - }, - } - }), - IsValid: true, - }, - "sad_path_distribution_nil": { - Expected: nil, - Input: nil, - IsValid: false, - }, - "sad_path_project_id_missing": { - Expected: expectedModel(), - Input: distributionFixture(func(d *cdn.Distribution) { - d.ProjectId = nil - }), - IsValid: false, - }, - "sad_path_distribution_id_missing": { - Expected: expectedModel(), - Input: distributionFixture(func(d *cdn.Distribution) { - d.Id = nil - }), - IsValid: false, - }, - } - for tn, tc := range tests { - t.Run(tn, func(t *testing.T) { - model := &Model{} - err := mapFields(context.Background(), tc.Input, model) - if err != nil && tc.IsValid { - t.Fatalf("Error mapping fields: %v", err) - } - if err == nil && !tc.IsValid { - t.Fatalf("Should have failed") - } - if tc.IsValid { - diff := cmp.Diff(model, tc.Expected) - if diff != "" { - t.Fatalf("Create Payload not as expected: %s", diff) - } - } - }) - } -} - -// TestValidateCountryCode tests the validateCountryCode function with a variety of inputs. -func TestValidateCountryCode(t *testing.T) { - testCases := []struct { - name string - inputCountry string - wantOutput string - expectError bool - expectedError string - }{ - // Happy Path - { - name: "Valid lowercase", - inputCountry: "us", - wantOutput: "US", - expectError: false, - }, - { - name: "Valid uppercase", - inputCountry: "DE", - wantOutput: "DE", - expectError: false, - }, - { - name: "Valid mixed case", - inputCountry: "cA", - wantOutput: "CA", - expectError: false, - }, - { - name: "Valid country code FR", - inputCountry: "fr", - wantOutput: "FR", - expectError: false, - }, - - // Error Scenarios - { - name: "Invalid length - too short", - inputCountry: "a", - wantOutput: "", - expectError: true, - expectedError: "country code must be exactly 2 characters long", - }, - { - name: "Invalid length - too long", - inputCountry: "USA", - wantOutput: "", - expectError: true, - expectedError: "country code must be exactly 2 characters long", - }, - { - name: "Invalid characters - contains number", - inputCountry: "U1", - wantOutput: "", - expectError: true, - expectedError: "country code 'U1' must consist of two alphabetical letters (A-Z or a-z)", - }, - { - name: "Invalid characters - contains symbol", - inputCountry: "D!", - wantOutput: "", - expectError: true, - expectedError: "country code 'D!' must consist of two alphabetical letters (A-Z or a-z)", - }, - { - name: "Invalid characters - both are numbers", - inputCountry: "42", - wantOutput: "", - expectError: true, - expectedError: "country code '42' must consist of two alphabetical letters (A-Z or a-z)", - }, - { - name: "Empty string", - inputCountry: "", - wantOutput: "", - expectError: true, - expectedError: "country code must be exactly 2 characters long", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - gotOutput, err := validateCountryCode(tc.inputCountry) - - if tc.expectError { - if err == nil { - t.Errorf("expected an error for input '%s', but got none", tc.inputCountry) - } else if err.Error() != tc.expectedError { - t.Errorf("for input '%s', expected error '%s', but got '%s'", tc.inputCountry, tc.expectedError, err.Error()) - } - if gotOutput != "" { - t.Errorf("expected empty string on error, but got '%s'", gotOutput) - } - } else { - if err != nil { - t.Errorf("did not expect an error for input '%s', but got: %v", tc.inputCountry, err) - } - if gotOutput != tc.wantOutput { - t.Errorf("for input '%s', expected output '%s', but got '%s'", tc.inputCountry, tc.wantOutput, gotOutput) - } - } - }) - } -} diff --git a/stackit/internal/services/cdn/utils/util.go b/stackit/internal/services/cdn/utils/util.go deleted file mode 100644 index e03f68d6..00000000 --- a/stackit/internal/services/cdn/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *cdn.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.CdnCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.CdnCustomEndpoint)) - } - apiClient, err := cdn.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/cdn/utils/util_test.go b/stackit/internal/services/cdn/utils/util_test.go deleted file mode 100644 index 576d0247..00000000 --- a/stackit/internal/services/cdn/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/cdn" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://cdn-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *cdn.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *cdn.APIClient { - apiClient, err := cdn.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - CdnCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *cdn.APIClient { - apiClient, err := cdn.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/dns/dns_acc_test.go b/stackit/internal/services/dns/dns_acc_test.go deleted file mode 100644 index 201ab118..00000000 --- a/stackit/internal/services/dns/dns_acc_test.go +++ /dev/null @@ -1,541 +0,0 @@ -package dns_test - -import ( - "context" - _ "embed" - "fmt" - "maps" - "regexp" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - core_config "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/resource-min.tf - resourceMinConfig string - - //go:embed testdata/resource-max.tf - resourceMaxConfig string -) - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "dns_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha) + ".example.home"), - "record_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "record_record1": config.StringVariable("1.2.3.4"), - "record_type": config.StringVariable("A"), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "dns_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha) + ".example.home"), - "acl": config.StringVariable("0.0.0.0/0"), - "active": config.BoolVariable(true), - "contact_email": config.StringVariable("contact@example.com"), - "default_ttl": config.IntegerVariable(3600), - "description": config.StringVariable("a test description"), - "expire_time": config.IntegerVariable(1 * 24 * 60 * 60), - "is_reverse_zone": config.BoolVariable(false), - // "negative_cache": config.IntegerVariable(128), - "primaries": config.ListVariable(config.StringVariable("1.1.1.1")), - "refresh_time": config.IntegerVariable(3600), - "retry_time": config.IntegerVariable(600), - "type": config.StringVariable("primary"), - - "record_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "record_record1": config.StringVariable("1.2.3.4"), - "record_active": config.BoolVariable(true), - "record_comment": config.StringVariable("a test comment"), - "record_ttl": config.IntegerVariable(3600), - "record_type": config.StringVariable("A"), -} - -func configVarsInvalid(vars config.Variables) config.Variables { - tempConfig := maps.Clone(vars) - tempConfig["dns_name"] = config.StringVariable("foo") - return tempConfig -} - -func configVarsMinUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMin) - tempConfig["record_record1"] = config.StringVariable("1.2.3.5") - - return tempConfig -} - -func configVarsMaxUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMax) - tempConfig["record_record1"] = config.StringVariable("1.2.3.5") - return tempConfig -} - -func TestAccDnsMinResource(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDnsDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: resourceMinConfig, - ConfigVariables: configVarsInvalid(testConfigVarsMin), - ExpectError: regexp.MustCompile(`not a valid dns name. Need at least two levels`), - }, - // creation - { - Config: resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Zone data - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - // Record set data - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "project_id", - "stackit_dns_zone.zone", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "zone_id", - "stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "name"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMin["record_record1"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMin["record_type"])), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state"), - ), - }, - // Data sources - { - Config: resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Zone data by zone_id - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrPair( - "stackit_dns_zone.zone", "zone_id", - "data.stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "zone_id", - "data.stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "project_id", - "data.stackit_dns_zone.zone", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "project_id", - "data.stackit_dns_record_set.record_set", "project_id", - ), - - // Zone data by dns_name - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrPair( - "stackit_dns_zone.zone", "zone_id", - "data.stackit_dns_zone.zone_name", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "zone_id", - "data.stackit_dns_zone.zone_name", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "project_id", - "data.stackit_dns_zone.zone_name", "project_id", - ), - - // Record set data - resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "record_set_id"), - resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "name"), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.#", "1"), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMin["record_record1"])), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMin["record_type"])), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_dns_zone.zone", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_dns_zone.zone"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_dns_zone.recozonerd_set") - } - zoneId, ok := r.Primary.Attributes["zone_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute zone_id") - } - - return fmt.Sprintf("%s,%s", testutil.ProjectId, zoneId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_dns_record_set.record_set", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_dns_record_set.record_set"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_dns_record_set.record_set") - } - zoneId, ok := r.Primary.Attributes["zone_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute zone_id") - } - recordSetId, ok := r.Primary.Attributes["record_set_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute record_set_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, zoneId, recordSetId), nil - }, - ImportState: true, - ImportStateVerify: true, - // Will be different because of the name vs fqdn problem, but the value is already tested in the datasource acc test - ImportStateVerifyIgnore: []string{"name"}, - }, - // Update. The zone ttl should not be updated according to the DNS API. - { - Config: resourceMinConfig, - ConfigVariables: configVarsMinUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Zone data - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - // Record set data - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "project_id", - "stackit_dns_zone.zone", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "zone_id", - "stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "name", testutil.ConvertConfigVariable(testConfigVarsMin["record_name"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(configVarsMinUpdated()["record_record1"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMin["record_type"])), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state")), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccDnsMaxResource(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDnsDestroy, - Steps: []resource.TestStep{ - // Creation - { - Config: resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Zone data - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - - // Record set data - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "project_id", - "stackit_dns_zone.zone", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "zone_id", - "stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])), - // resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "negative_cache"), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "primaries.#", "1"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primaries.0"), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "name", testutil.ConvertConfigVariable(testConfigVarsMax["record_name"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMax["record_record1"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["record_active"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "comment", testutil.ConvertConfigVariable(testConfigVarsMax["record_comment"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "ttl", testutil.ConvertConfigVariable(testConfigVarsMax["record_ttl"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMax["record_type"])), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state"), - ), - }, - // Data sources - { - Config: resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Zone data by zone_id - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrPair( - "stackit_dns_zone.zone", "zone_id", - "data.stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "zone_id", - "data.stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "project_id", - "data.stackit_dns_zone.zone", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "project_id", - "stackit_dns_record_set.record_set", "project_id", - ), - - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "primaries.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "primaries.0"), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "dns_name", testutil.ConvertConfigVariable(testConfigVarsMax["dns_name"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - // resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "negative_cache"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "serial_number"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "state"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "visibility"), - - // Zone data by dns_name - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrPair( - "stackit_dns_zone.zone", "zone_id", - "data.stackit_dns_zone.zone_name", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "zone_id", - "data.stackit_dns_zone.zone_name", "zone_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_dns_record_set.record_set", "project_id", - "data.stackit_dns_zone.zone_name", "project_id", - ), - - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "primaries.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "primaries.0"), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "dns_name", testutil.ConvertConfigVariable(testConfigVarsMax["dns_name"])), - resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - // resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "negative_cache"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "serial_number"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "state"), - resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "visibility"), - - // Record set data - resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "record_set_id"), - resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "name"), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])), - resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "fqdn"), - resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "state"), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.#", "1"), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMax["record_record1"])), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["record_active"])), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "comment", testutil.ConvertConfigVariable(testConfigVarsMax["record_comment"])), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "ttl", testutil.ConvertConfigVariable(testConfigVarsMax["record_ttl"])), - resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMax["record_type"])), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_dns_zone.zone", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_dns_zone.zone"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_dns_zone.record_set") - } - zoneId, ok := r.Primary.Attributes["zone_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute zone_id") - } - - return fmt.Sprintf("%s,%s", testutil.ProjectId, zoneId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_dns_record_set.record_set", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_dns_record_set.record_set"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_dns_record_set.record_set") - } - zoneId, ok := r.Primary.Attributes["zone_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute zone_id") - } - recordSetId, ok := r.Primary.Attributes["record_set_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute record_set_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, zoneId, recordSetId), nil - }, - ImportState: true, - ImportStateVerify: true, - // Will be different because of the name vs fqdn problem, but the value is already tested in the datasource acc test - ImportStateVerifyIgnore: []string{"name"}, - }, - // Update. The zone ttl should not be updated according to the DNS API. - { - Config: resourceMaxConfig, - ConfigVariables: configVarsMaxUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Zone data - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])), - // resource.TestCheckResourceAttr("stackit_dns_zone.zone", "negative_cache", testutil.ConvertConfigVariable(testConfigVarsMax["negative_cache"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "primaries.#", "1"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primaries.0"), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])), - resource.TestCheckResourceAttr("stackit_dns_zone.zone", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"), - resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"), - // Record set data - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "project_id", - "stackit_dns_zone.zone", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_dns_record_set.record_set", "zone_id", - "stackit_dns_zone.zone", "zone_id", - ), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "name", testutil.ConvertConfigVariable(testConfigVarsMax["record_name"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["record_record1"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["record_active"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "comment", testutil.ConvertConfigVariable(testConfigVarsMax["record_comment"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "ttl", testutil.ConvertConfigVariable(testConfigVarsMax["record_ttl"])), - resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMax["record_type"])), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"), - resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state")), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckDnsDestroy(s *terraform.State) error { - ctx := context.Background() - var client *dns.APIClient - var err error - if testutil.DnsCustomEndpoint == "" { - client, err = dns.NewAPIClient() - } else { - client, err = dns.NewAPIClient( - core_config.WithEndpoint(testutil.DnsCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - zonesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_dns_zone" { - continue - } - // zone terraform ID: "[projectId],[zoneId]" - zoneId := strings.Split(rs.Primary.ID, core.Separator)[1] - zonesToDestroy = append(zonesToDestroy, zoneId) - } - - zonesResp, err := client.ListZones(ctx, testutil.ProjectId).ActiveEq(true).Execute() - if err != nil { - return fmt.Errorf("getting zonesResp: %w", err) - } - - zones := *zonesResp.Zones - for i := range zones { - id := *zones[i].Id - if utils.Contains(zonesToDestroy, id) { - _, err := client.DeleteZoneExecute(ctx, testutil.ProjectId, id) - if err != nil { - return fmt.Errorf("destroying zone %s during CheckDestroy: %w", *zones[i].Id, err) - } - _, err = wait.DeleteZoneWaitHandler(ctx, client, testutil.ProjectId, id).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying zone %s during CheckDestroy: waiting for deletion %w", *zones[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/dns/recordset/datasource.go b/stackit/internal/services/dns/recordset/datasource.go deleted file mode 100644 index b7fb1b1c..00000000 --- a/stackit/internal/services/dns/recordset/datasource.go +++ /dev/null @@ -1,183 +0,0 @@ -package dns - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &recordSetDataSource{} -) - -// NewRecordSetDataSource NewZoneDataSource is a helper function to simplify the provider implementation. -func NewRecordSetDataSource() datasource.DataSource { - return &recordSetDataSource{} -} - -// recordSetDataSource is the data source implementation. -type recordSetDataSource struct { - client *dns.APIClient -} - -// Metadata returns the data source type name. -func (d *recordSetDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_dns_record_set" -} - -// Configure adds the provider configured client to the data source. -func (d *recordSetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "DNS record set client configured") -} - -// Schema defines the schema for the data source. -func (d *recordSetDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "DNS Record Set Resource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source. ID. It is structured as \"`project_id`,`zone_id`,`record_set_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the dns record set is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "zone_id": schema.StringAttribute{ - Description: "The zone ID to which is dns record set is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "record_set_id": schema.StringAttribute{ - Description: "The rr set id.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "Name of the record which should be a valid domain according to rfc1035 Section 2.3.4. E.g. `example.com`", - Computed: true, - }, - "fqdn": schema.StringAttribute{ - Description: "Fully qualified domain name (FQDN) of the record set.", - Computed: true, - }, - "records": schema.ListAttribute{ - Description: "Records.", - Computed: true, - ElementType: types.StringType, - }, - "ttl": schema.Int64Attribute{ - Description: "Time to live. E.g. 3600", - Computed: true, - }, - "type": schema.StringAttribute{ - Description: "The record set type. E.g. `A` or `CNAME`", - Computed: true, - }, - "active": schema.BoolAttribute{ - Description: "Specifies if the record set is active or not.", - Computed: true, - }, - "comment": schema.StringAttribute{ - Description: "Comment.", - Computed: true, - }, - "error": schema.StringAttribute{ - Description: "Error shows error in case create/update/delete failed.", - Computed: true, - }, - "state": schema.StringAttribute{ - Description: "Record set state.", - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - recordSetId := model.RecordSetId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - ctx = tflog.SetField(ctx, "record_set_id", recordSetId) - recordSetResp, err := d.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading record set", - fmt.Sprintf("The record set %q or zone %q does not exist in project %q.", recordSetId, zoneId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - if recordSetResp != nil && recordSetResp.Rrset.State != nil && *recordSetResp.Rrset.State == dns.RECORDSETSTATE_DELETE_SUCCEEDED { - resp.State.RemoveResource(ctx) - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", "Record set was deleted successfully") - return - } - - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS record set read") -} diff --git a/stackit/internal/services/dns/recordset/resource.go b/stackit/internal/services/dns/recordset/resource.go deleted file mode 100644 index 2077e09f..00000000 --- a/stackit/internal/services/dns/recordset/resource.go +++ /dev/null @@ -1,515 +0,0 @@ -package dns - -import ( - "context" - "fmt" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &recordSetResource{} - _ resource.ResourceWithConfigure = &recordSetResource{} - _ resource.ResourceWithImportState = &recordSetResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - RecordSetId types.String `tfsdk:"record_set_id"` - ZoneId types.String `tfsdk:"zone_id"` - ProjectId types.String `tfsdk:"project_id"` - Active types.Bool `tfsdk:"active"` - Comment types.String `tfsdk:"comment"` - Name types.String `tfsdk:"name"` - Records types.List `tfsdk:"records"` - TTL types.Int64 `tfsdk:"ttl"` - Type types.String `tfsdk:"type"` - Error types.String `tfsdk:"error"` - State types.String `tfsdk:"state"` - FQDN types.String `tfsdk:"fqdn"` -} - -// NewRecordSetResource is a helper function to simplify the provider implementation. -func NewRecordSetResource() resource.Resource { - return &recordSetResource{} -} - -// recordSetResource is the resource implementation. -type recordSetResource struct { - client *dns.APIClient -} - -// Metadata returns the resource type name. -func (r *recordSetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_dns_record_set" -} - -// Configure adds the provider configured client to the resource. -func (r *recordSetResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "DNS record set client configured") -} - -// Schema defines the schema for the resource. -func (r *recordSetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "DNS Record Set Resource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`zone_id`,`record_set_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the dns record set is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "zone_id": schema.StringAttribute{ - Description: "The zone ID to which is dns record set is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "record_set_id": schema.StringAttribute{ - Description: "The rr set id.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "Name of the record which should be a valid domain according to rfc1035 Section 2.3.4. E.g. `example.com`", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "fqdn": schema.StringAttribute{ - Description: "Fully qualified domain name (FQDN) of the record set.", - Computed: true, - }, - "records": schema.ListAttribute{ - Description: "Records.", - ElementType: types.StringType, - Required: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - listvalidator.UniqueValues(), - listvalidator.ValueStringsAre(validate.RecordSet()), - }, - }, - "ttl": schema.Int64Attribute{ - Description: "Time to live. E.g. 3600", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(60), - int64validator.AtMost(99999999), - }, - }, - "type": schema.StringAttribute{ - Description: "The record set type. E.g. `A` or `CNAME`", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "active": schema.BoolAttribute{ - Description: "Specifies if the record set is active or not. Defaults to `true`", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - }, - "comment": schema.StringAttribute{ - Description: "Comment.", - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtMost(255), - }, - }, - "error": schema.StringAttribute{ - Description: "Error shows error in case create/update/delete failed.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtMost(2000), - }, - }, - "state": schema.StringAttribute{ - Description: "Record set state.", - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *recordSetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new recordset - recordSetResp, err := r.client.CreateRecordSet(ctx, projectId, zoneId).CreateRecordSetPayload(*payload).Execute() - if err != nil || recordSetResp.Rrset == nil || recordSetResp.Rrset.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": projectId, - "zone_id": zoneId, - "record_set_id": *recordSetResp.Rrset.Id, - }) - if resp.Diagnostics.HasError() { - return - } - - waitResp, err := wait.CreateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, *recordSetResp.Rrset.Id).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS record set created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - recordSetId := model.RecordSetId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - ctx = tflog.SetField(ctx, "record_set_id", recordSetId) - - recordSetResp, err := r.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Calling API: %v", err)) - return - } - if recordSetResp != nil && recordSetResp.Rrset.State != nil && *recordSetResp.Rrset.State == dns.RECORDSETSTATE_DELETE_SUCCEEDED { - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS record set read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - recordSetId := model.RecordSetId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - ctx = tflog.SetField(ctx, "record_set_id", recordSetId) - - // Generate API request body from model - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update recordset - _, err = r.client.PartialUpdateRecordSet(ctx, projectId, zoneId, recordSetId).PartialUpdateRecordSetPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.PartialUpdateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS record set updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - recordSetId := model.RecordSetId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - ctx = tflog.SetField(ctx, "record_set_id", recordSetId) - - // Delete existing record set - _, err := r.client.DeleteRecordSet(ctx, projectId, zoneId, recordSetId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "DNS record set deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *recordSetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing record set", - fmt.Sprintf("Expected import identifier with format [project_id],[zone_id],[record_set_id], got %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ - "project_id": idParts[0], - "zone_id": idParts[1], - "record_set_id": idParts[2], - }) - tflog.Info(ctx, "DNS record set state imported") -} - -func mapFields(ctx context.Context, recordSetResp *dns.RecordSetResponse, model *Model) error { - if recordSetResp == nil || recordSetResp.Rrset == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - recordSet := recordSetResp.Rrset - - var recordSetId string - if model.RecordSetId.ValueString() != "" { - recordSetId = model.RecordSetId.ValueString() - } else if recordSet.Id != nil { - recordSetId = *recordSet.Id - } else { - return fmt.Errorf("record set id not present") - } - - if recordSet.Records == nil { - model.Records = types.ListNull(types.StringType) - } else { - respRecords := []string{} - - for _, record := range *recordSet.Records { - respRecords = append(respRecords, *record.Content) - } - - modelRecords, err := utils.ListValuetoStringSlice(model.Records) - if err != nil { - return err - } - - reconciledRecords := utils.ReconcileStringSlices(modelRecords, respRecords) - - recordsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledRecords) - if diags.HasError() { - return fmt.Errorf("failed to map records: %w", core.DiagsToError(diags)) - } - - model.Records = recordsTF - } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), model.ZoneId.ValueString(), recordSetId, - ) - model.RecordSetId = types.StringPointerValue(recordSet.Id) - model.Active = types.BoolPointerValue(recordSet.Active) - model.Comment = types.StringPointerValue(recordSet.Comment) - model.Error = types.StringPointerValue(recordSet.Error) - if model.Name.IsNull() || model.Name.IsUnknown() { - model.Name = types.StringPointerValue(recordSet.Name) - } - model.FQDN = types.StringPointerValue(recordSet.Name) - model.State = types.StringValue(string(recordSet.GetState())) - model.TTL = types.Int64PointerValue(recordSet.Ttl) - model.Type = types.StringValue(string(recordSet.GetType())) - return nil -} - -func toCreatePayload(model *Model) (*dns.CreateRecordSetPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - records := []dns.RecordPayload{} - for i, record := range model.Records.Elements() { - recordString, ok := record.(types.String) - if !ok { - return nil, fmt.Errorf("expected record at index %d to be of type %T, got %T", i, types.String{}, record) - } - records = append(records, dns.RecordPayload{ - Content: conversion.StringValueToPointer(recordString), - }) - } - - return &dns.CreateRecordSetPayload{ - Comment: conversion.StringValueToPointer(model.Comment), - Name: conversion.StringValueToPointer(model.Name), - Records: &records, - Ttl: conversion.Int64ValueToPointer(model.TTL), - Type: dns.CreateRecordSetPayloadGetTypeAttributeType(conversion.StringValueToPointer(model.Type)), - }, nil -} - -func toUpdatePayload(model *Model) (*dns.PartialUpdateRecordSetPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - records := []dns.RecordPayload{} - for i, record := range model.Records.Elements() { - recordString, ok := record.(types.String) - if !ok { - return nil, fmt.Errorf("expected record at index %d to be of type %T, got %T", i, types.String{}, record) - } - records = append(records, dns.RecordPayload{ - Content: conversion.StringValueToPointer(recordString), - }) - } - - return &dns.PartialUpdateRecordSetPayload{ - Comment: conversion.StringValueToPointer(model.Comment), - Name: conversion.StringValueToPointer(model.Name), - Records: &records, - Ttl: conversion.Int64ValueToPointer(model.TTL), - }, nil -} diff --git a/stackit/internal/services/dns/recordset/resource_test.go b/stackit/internal/services/dns/recordset/resource_test.go deleted file mode 100644 index f73309a0..00000000 --- a/stackit/internal/services/dns/recordset/resource_test.go +++ /dev/null @@ -1,375 +0,0 @@ -package dns - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *dns.RecordSetResponse - expected Model - isValid bool - }{ - { - "default_values", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - &dns.RecordSetResponse{ - Rrset: &dns.RecordSet{ - Id: utils.Ptr("rid"), - }, - }, - Model{ - Id: types.StringValue("pid,zid,rid"), - RecordSetId: types.StringValue("rid"), - ZoneId: types.StringValue("zid"), - ProjectId: types.StringValue("pid"), - Active: types.BoolNull(), - Comment: types.StringNull(), - Error: types.StringNull(), - Name: types.StringNull(), - FQDN: types.StringNull(), - Records: types.ListNull(types.StringType), - State: types.StringValue(""), - TTL: types.Int64Null(), - Type: types.StringValue(""), - }, - true, - }, - { - "simple_values", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - &dns.RecordSetResponse{ - Rrset: &dns.RecordSet{ - Id: utils.Ptr("rid"), - Active: utils.Ptr(true), - Comment: utils.Ptr("comment"), - Error: utils.Ptr("error"), - Name: utils.Ptr("name"), - Records: &[]dns.Record{ - {Content: utils.Ptr("record_1")}, - {Content: utils.Ptr("record_2")}, - }, - State: dns.RECORDSETSTATE_CREATING.Ptr(), - Ttl: utils.Ptr(int64(1)), - Type: dns.RECORDSETTYPE_A.Ptr(), - }, - }, - Model{ - Id: types.StringValue("pid,zid,rid"), - RecordSetId: types.StringValue("rid"), - ZoneId: types.StringValue("zid"), - ProjectId: types.StringValue("pid"), - Active: types.BoolValue(true), - Comment: types.StringValue("comment"), - Error: types.StringValue("error"), - Name: types.StringValue("name"), - FQDN: types.StringValue("name"), - Records: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("record_1"), - types.StringValue("record_2"), - }), - State: types.StringValue(string(dns.RECORDSETSTATE_CREATING)), - TTL: types.Int64Value(1), - Type: types.StringValue(string(dns.RECORDSETTYPE_A)), - }, - true, - }, - { - "unordered_records", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - Records: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("record_2"), - types.StringValue("record_1"), - }), - }, - &dns.RecordSetResponse{ - Rrset: &dns.RecordSet{ - Id: utils.Ptr("rid"), - Active: utils.Ptr(true), - Comment: utils.Ptr("comment"), - Error: utils.Ptr("error"), - Name: utils.Ptr("name"), - Records: &[]dns.Record{ - {Content: utils.Ptr("record_1")}, - {Content: utils.Ptr("record_2")}, - }, - State: dns.RECORDSETSTATE_CREATING.Ptr(), - Ttl: utils.Ptr(int64(1)), - Type: dns.RECORDSETTYPE_A.Ptr(), - }, - }, - Model{ - Id: types.StringValue("pid,zid,rid"), - RecordSetId: types.StringValue("rid"), - ZoneId: types.StringValue("zid"), - ProjectId: types.StringValue("pid"), - Active: types.BoolValue(true), - Comment: types.StringValue("comment"), - Error: types.StringValue("error"), - Name: types.StringValue("name"), - FQDN: types.StringValue("name"), - Records: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("record_2"), - types.StringValue("record_1"), - }), - State: types.StringValue(string(dns.RECORDSETSTATE_CREATING)), - TTL: types.Int64Value(1), - Type: types.StringValue(string(dns.RECORDSETTYPE_A)), - }, - true, - }, - { - "null_fields_and_int_conversions", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - Name: types.StringValue("other-name"), - }, - &dns.RecordSetResponse{ - Rrset: &dns.RecordSet{ - Id: utils.Ptr("rid"), - Active: nil, - Comment: nil, - Error: nil, - Name: utils.Ptr("name"), - Records: nil, - State: dns.RECORDSETSTATE_CREATING.Ptr(), - Ttl: utils.Ptr(int64(2123456789)), - Type: dns.RECORDSETTYPE_A.Ptr(), - }, - }, - Model{ - Id: types.StringValue("pid,zid,rid"), - RecordSetId: types.StringValue("rid"), - ZoneId: types.StringValue("zid"), - ProjectId: types.StringValue("pid"), - Active: types.BoolNull(), - Comment: types.StringNull(), - Error: types.StringNull(), - Name: types.StringValue("other-name"), - FQDN: types.StringValue("name"), - Records: types.ListNull(types.StringType), - State: types.StringValue(string(dns.RECORDSETSTATE_CREATING)), - TTL: types.Int64Value(2123456789), - Type: types.StringValue(string(dns.RECORDSETTYPE_A)), - }, - true, - }, - { - "nil_response", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - &dns.RecordSetResponse{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *dns.CreateRecordSetPayload - isValid bool - }{ - { - "default values", - &Model{}, - &dns.CreateRecordSetPayload{ - Records: &[]dns.RecordPayload{}, - }, - true, - }, - { - "simple_values", - &Model{ - Comment: types.StringValue("comment"), - Name: types.StringValue("name"), - Records: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("record_1"), - types.StringValue("record_2"), - }), - TTL: types.Int64Value(1), - Type: types.StringValue(string(dns.RECORDSETTYPE_A)), - }, - &dns.CreateRecordSetPayload{ - Comment: utils.Ptr("comment"), - Name: utils.Ptr("name"), - Records: &[]dns.RecordPayload{ - {Content: utils.Ptr("record_1")}, - {Content: utils.Ptr("record_2")}, - }, - Ttl: utils.Ptr(int64(1)), - Type: dns.CREATERECORDSETPAYLOADTYPE_A.Ptr(), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Comment: types.StringNull(), - Name: types.StringValue(""), - Records: types.ListValueMust(types.StringType, nil), - TTL: types.Int64Value(2123456789), - Type: types.StringValue(string(dns.RECORDSETTYPE_A)), - }, - &dns.CreateRecordSetPayload{ - Comment: nil, - Name: utils.Ptr(""), - Records: &[]dns.RecordPayload{}, - Ttl: utils.Ptr(int64(2123456789)), - Type: dns.CREATERECORDSETPAYLOADTYPE_A.Ptr(), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *dns.PartialUpdateRecordSetPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &dns.PartialUpdateRecordSetPayload{ - Records: &[]dns.RecordPayload{}, - }, - true, - }, - { - "simple_values", - &Model{ - Comment: types.StringValue("comment"), - Name: types.StringValue("name"), - Records: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("record_1"), - types.StringValue("record_2"), - }), - TTL: types.Int64Value(1), - }, - &dns.PartialUpdateRecordSetPayload{ - Comment: utils.Ptr("comment"), - Name: utils.Ptr("name"), - Records: &[]dns.RecordPayload{ - {Content: utils.Ptr("record_1")}, - {Content: utils.Ptr("record_2")}, - }, - Ttl: utils.Ptr(int64(1)), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Comment: types.StringNull(), - Name: types.StringValue(""), - Records: types.ListValueMust(types.StringType, nil), - TTL: types.Int64Value(2123456789), - }, - &dns.PartialUpdateRecordSetPayload{ - Comment: nil, - Name: utils.Ptr(""), - Records: &[]dns.RecordPayload{}, - Ttl: utils.Ptr(int64(2123456789)), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/dns/testdata/resource-max.tf b/stackit/internal/services/dns/testdata/resource-max.tf deleted file mode 100644 index 27d6894e..00000000 --- a/stackit/internal/services/dns/testdata/resource-max.tf +++ /dev/null @@ -1,74 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "dns_name" {} -variable "acl" {} -variable "active" {} -variable "contact_email" {} -variable "default_ttl" {} -variable "description" {} -variable "expire_time" {} -variable "is_reverse_zone" {} -# variable "negative_cache" {} -variable "primaries" {} -variable "refresh_time" {} -variable "retry_time" {} -variable "type" {} - -variable "record_name" {} -variable "record_record1" {} -variable "record_active" {} -variable "record_comment" {} -variable "record_ttl" {} -variable "record_type" {} - - - - -resource "stackit_dns_zone" "zone" { - project_id = var.project_id - name = var.name - dns_name = var.dns_name - acl = var.acl - active = var.active - contact_email = var.contact_email - default_ttl = var.default_ttl - description = var.description - expire_time = var.expire_time - is_reverse_zone = var.is_reverse_zone - # negative_cache = var.negative_cache - primaries = var.primaries - refresh_time = var.refresh_time - retry_time = var.retry_time - type = var.type -} - - -resource "stackit_dns_record_set" "record_set" { - project_id = var.project_id - zone_id = stackit_dns_zone.zone.zone_id - name = var.record_name - records = [ - var.record_record1 - ] - - active = var.record_active - comment = var.record_comment - ttl = var.record_ttl - type = var.record_type -} - -data "stackit_dns_zone" "zone" { - project_id = var.project_id - zone_id = stackit_dns_zone.zone.zone_id -} - -data "stackit_dns_zone" "zone_name" { - project_id = var.project_id - dns_name = stackit_dns_zone.zone.dns_name -} - -data "stackit_dns_record_set" "record_set" { - project_id = var.project_id - zone_id = stackit_dns_zone.zone.zone_id - record_set_id = stackit_dns_record_set.record_set.record_set_id -} diff --git a/stackit/internal/services/dns/testdata/resource-min.tf b/stackit/internal/services/dns/testdata/resource-min.tf deleted file mode 100644 index 2a99b33c..00000000 --- a/stackit/internal/services/dns/testdata/resource-min.tf +++ /dev/null @@ -1,41 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "dns_name" {} - -variable "record_name" {} -variable "record_record1" {} -variable "record_type" {} - - -resource "stackit_dns_zone" "zone" { - project_id = var.project_id - name = var.name - dns_name = var.dns_name -} - - -resource "stackit_dns_record_set" "record_set" { - project_id = var.project_id - zone_id = stackit_dns_zone.zone.zone_id - name = var.record_name - records = [ - var.record_record1 - ] - type = var.record_type -} - -data "stackit_dns_zone" "zone" { - project_id = var.project_id - zone_id = stackit_dns_zone.zone.zone_id -} - -data "stackit_dns_zone" "zone_name" { - project_id = var.project_id - dns_name = stackit_dns_zone.zone.dns_name -} - -data "stackit_dns_record_set" "record_set" { - project_id = var.project_id - zone_id = stackit_dns_zone.zone.zone_id - record_set_id = stackit_dns_record_set.record_set.record_set_id -} diff --git a/stackit/internal/services/dns/utils/util.go b/stackit/internal/services/dns/utils/util.go deleted file mode 100644 index ca0e4889..00000000 --- a/stackit/internal/services/dns/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *dns.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.DnsCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.DnsCustomEndpoint)) - } - apiClient, err := dns.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/dns/utils/util_test.go b/stackit/internal/services/dns/utils/util_test.go deleted file mode 100644 index 31e61382..00000000 --- a/stackit/internal/services/dns/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://dns-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *dns.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *dns.APIClient { - apiClient, err := dns.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - DnsCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *dns.APIClient { - apiClient, err := dns.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/dns/zone/datasource.go b/stackit/internal/services/dns/zone/datasource.go deleted file mode 100644 index 645ae943..00000000 --- a/stackit/internal/services/dns/zone/datasource.go +++ /dev/null @@ -1,268 +0,0 @@ -package dns - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &zoneDataSource{} -) - -// NewZoneDataSource is a helper function to simplify the provider implementation. -func NewZoneDataSource() datasource.DataSource { - return &zoneDataSource{} -} - -// zoneDataSource is the data source implementation. -type zoneDataSource struct { - client *dns.APIClient -} - -// Metadata returns the data source type name. -func (d *zoneDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_dns_zone" -} - -// ConfigValidators validates the resource configuration -func (d *zoneDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator { - return []datasource.ConfigValidator{ - datasourcevalidator.ExactlyOneOf( - path.MatchRoot("zone_id"), - path.MatchRoot("dns_name"), - ), - } -} - -func (d *zoneDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "DNS zone client configured") -} - -// Schema defines the schema for the data source. -func (d *zoneDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "DNS Zone resource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source. ID. It is structured as \"`project_id`,`zone_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the dns zone is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "zone_id": schema.StringAttribute{ - Description: "The zone ID.", - Optional: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The user given name of the zone.", - Computed: true, - }, - "dns_name": schema.StringAttribute{ - Description: "The zone name. E.g. `example.com`", - Optional: true, - }, - "description": schema.StringAttribute{ - Description: "Description of the zone.", - Computed: true, - }, - "acl": schema.StringAttribute{ - Description: "The access control list.", - Computed: true, - }, - "active": schema.BoolAttribute{ - Description: "", - Computed: true, - }, - "contact_email": schema.StringAttribute{ - Description: "A contact e-mail for the zone.", - Computed: true, - }, - "default_ttl": schema.Int64Attribute{ - Description: "Default time to live.", - Computed: true, - }, - "expire_time": schema.Int64Attribute{ - Description: "Expire time.", - Computed: true, - }, - "is_reverse_zone": schema.BoolAttribute{ - Description: "Specifies, if the zone is a reverse zone or not.", - Computed: true, - }, - "negative_cache": schema.Int64Attribute{ - Description: "Negative caching.", - Computed: true, - }, - "primary_name_server": schema.StringAttribute{ - Description: "Primary name server. FQDN.", - Computed: true, - }, - "primaries": schema.ListAttribute{ - Description: `Primary name server for secondary zone.`, - Computed: true, - ElementType: types.StringType, - }, - "record_count": schema.Int64Attribute{ - Description: "Record count how many records are in the zone.", - Computed: true, - }, - "refresh_time": schema.Int64Attribute{ - Description: "Refresh time.", - Computed: true, - }, - "retry_time": schema.Int64Attribute{ - Description: "Retry time.", - Computed: true, - }, - "serial_number": schema.Int64Attribute{ - Description: "Serial number.", - Computed: true, - }, - "type": schema.StringAttribute{ - Description: "Zone type.", - Computed: true, - }, - "visibility": schema.StringAttribute{ - Description: "Visibility of the zone.", - Computed: true, - }, - "state": schema.StringAttribute{ - Description: "Zone state.", - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - dnsName := model.DnsName.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - ctx = tflog.SetField(ctx, "dns_name", dnsName) - - var zoneResp *dns.ZoneResponse - var err error - - if zoneId != "" { - zoneResp, err = d.client.GetZone(ctx, projectId, zoneId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading zone", - fmt.Sprintf("Zone with ID %q does not exist in project %q.", zoneId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - } else { - listZoneResp, err := d.client.ListZones(ctx, projectId). - DnsNameEq(dnsName). - ActiveEq(true). - Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading zone", - fmt.Sprintf("Zone with DNS name %q does not exist in project %q.", dnsName, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - if *listZoneResp.TotalItems != 1 { - utils.LogError( - ctx, - &resp.Diagnostics, - fmt.Errorf("zone with DNS name %q does not exist in project %q", dnsName, projectId), - "Reading zone", - fmt.Sprintf("Zone with DNS name %q does not exist in project %q.", dnsName, projectId), - nil, - ) - resp.State.RemoveResource(ctx) - return - } - zones := *listZoneResp.Zones - zoneResp = dns.NewZoneResponse(zones[0]) - } - - if zoneResp != nil && zoneResp.Zone.State != nil && *zoneResp.Zone.State == dns.ZONESTATE_DELETE_SUCCEEDED { - resp.State.RemoveResource(ctx) - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", "Zone was deleted successfully") - return - } - - err = mapFields(ctx, zoneResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS zone read") -} diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go deleted file mode 100644 index 9fd92436..00000000 --- a/stackit/internal/services/dns/zone/resource.go +++ /dev/null @@ -1,603 +0,0 @@ -package dns - -import ( - "context" - "fmt" - "math" - "strings" - - dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "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/listplanmodifier" - "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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &zoneResource{} - _ resource.ResourceWithConfigure = &zoneResource{} - _ resource.ResourceWithImportState = &zoneResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ZoneId types.String `tfsdk:"zone_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - DnsName types.String `tfsdk:"dns_name"` - Description types.String `tfsdk:"description"` - Acl types.String `tfsdk:"acl"` - Active types.Bool `tfsdk:"active"` - ContactEmail types.String `tfsdk:"contact_email"` - DefaultTTL types.Int64 `tfsdk:"default_ttl"` - ExpireTime types.Int64 `tfsdk:"expire_time"` - IsReverseZone types.Bool `tfsdk:"is_reverse_zone"` - NegativeCache types.Int64 `tfsdk:"negative_cache"` - PrimaryNameServer types.String `tfsdk:"primary_name_server"` - Primaries types.List `tfsdk:"primaries"` - RecordCount types.Int64 `tfsdk:"record_count"` - RefreshTime types.Int64 `tfsdk:"refresh_time"` - RetryTime types.Int64 `tfsdk:"retry_time"` - SerialNumber types.Int64 `tfsdk:"serial_number"` - Type types.String `tfsdk:"type"` - Visibility types.String `tfsdk:"visibility"` - State types.String `tfsdk:"state"` -} - -// NewZoneResource is a helper function to simplify the provider implementation. -func NewZoneResource() resource.Resource { - return &zoneResource{} -} - -// zoneResource is the resource implementation. -type zoneResource struct { - client *dns.APIClient -} - -// Metadata returns the resource type name. -func (r *zoneResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_dns_zone" -} - -// Configure adds the provider configured client to the resource. -func (r *zoneResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "DNS zone client configured") -} - -// Schema defines the schema for the resource. -func (r *zoneResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - primaryOptions := []string{"primary", "secondary"} - - resp.Schema = schema.Schema{ - Description: "DNS Zone resource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`zone_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the dns zone is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "zone_id": schema.StringAttribute{ - Description: "The zone ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The user given name of the zone.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "dns_name": schema.StringAttribute{ - Description: "The zone name. E.g. `example.com`", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(253), - }, - }, - "description": schema.StringAttribute{ - Description: "Description of the zone.", - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtMost(1024), - }, - }, - "acl": schema.StringAttribute{ - Description: "The access control list. E.g. `0.0.0.0/0,::/0`", - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtMost(2000), - }, - }, - "active": schema.BoolAttribute{ - Description: "", - Optional: true, - Computed: true, - }, - "contact_email": schema.StringAttribute{ - Description: "A contact e-mail for the zone.", - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtMost(255), - }, - }, - "default_ttl": schema.Int64Attribute{ - Description: "Default time to live. E.g. 3600.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(60, 99999999), - }, - }, - "expire_time": schema.Int64Attribute{ - Description: "Expire time. E.g. 1209600.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(60, 99999999), - }, - }, - "is_reverse_zone": schema.BoolAttribute{ - Description: "Specifies, if the zone is a reverse zone or not. Defaults to `false`", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - "negative_cache": schema.Int64Attribute{ - Description: "Negative caching. E.g. 60", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(60, 99999999), - }, - }, - "primaries": schema.ListAttribute{ - Description: `Primary name server for secondary zone. E.g. ["1.2.3.4"]`, - Optional: true, - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - listplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.List{ - listvalidator.SizeAtMost(10), - }, - }, - "refresh_time": schema.Int64Attribute{ - Description: "Refresh time. E.g. 3600", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(60, 99999999), - }, - }, - "retry_time": schema.Int64Attribute{ - Description: "Retry time. E.g. 600", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(60, 99999999), - }, - }, - "type": schema.StringAttribute{ - Description: "Zone type. Defaults to `primary`. " + utils.FormatPossibleValues(primaryOptions...), - Optional: true, - Computed: true, - Default: stringdefault.StaticString("primary"), - Validators: []validator.String{ - stringvalidator.OneOf(primaryOptions...), - }, - }, - "primary_name_server": schema.StringAttribute{ - Description: "Primary name server. FQDN.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(253), - }, - }, - "serial_number": schema.Int64Attribute{ - Description: "Serial number. E.g. `2022111400`.", - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - int64validator.AtMost(math.MaxInt32 - 1), - }, - }, - "visibility": schema.StringAttribute{ - Description: "Visibility of the zone. E.g. `public`.", - Computed: true, - }, - "record_count": schema.Int64Attribute{ - Description: "Record count how many records are in the zone.", - Computed: true, - }, - "state": schema.StringAttribute{ - Description: "Zone state. E.g. `CREATE_SUCCEEDED`.", - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new zone - createResp, err := r.client.CreateZone(ctx, projectId).CreateZonePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - zoneId := *createResp.Zone.Id - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ - "project_id": projectId, - "zone_id": zoneId, - }) - if resp.Diagnostics.HasError() { - return - } - - waitResp, err := wait.CreateZoneWaitHandler(ctx, r.client, projectId, zoneId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Zone creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating zone", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS zone created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - - zoneResp, err := r.client.GetZone(ctx, projectId, zoneId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if zoneResp != nil && zoneResp.Zone.State != nil && *zoneResp.Zone.State == dns.ZONESTATE_DELETE_SUCCEEDED { - resp.State.RemoveResource(ctx) - return - } - - // Map response body to schema - err = mapFields(ctx, zoneResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS zone read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - - // Generate API request body from model - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing zone - _, err = r.client.PartialUpdateZone(ctx, projectId, zoneId).PartialUpdateZonePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.PartialUpdateZoneWaitHandler(ctx, r.client, projectId, zoneId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Zone update waiting: %v", err)) - return - } - - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "DNS zone updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - zoneId := model.ZoneId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "zone_id", zoneId) - - // Delete existing zone - _, err := r.client.DeleteZone(ctx, projectId, zoneId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteZoneWaitHandler(ctx, r.client, projectId, zoneId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", fmt.Sprintf("Zone deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "DNS zone deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id -func (r *zoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing zone", - fmt.Sprintf("Expected import identifier with format: [project_id],[zone_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ - "project_id": idParts[0], - "zone_id": idParts[1], - }) - - tflog.Info(ctx, "DNS zone state imported") -} - -func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) error { - if zoneResp == nil || zoneResp.Zone == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - z := zoneResp.Zone - - var rc *int64 - if z.RecordCount != nil { - recordCount64 := int64(*z.RecordCount) - rc = &recordCount64 - } else { - rc = nil - } - - var zoneId string - if model.ZoneId.ValueString() != "" { - zoneId = model.ZoneId.ValueString() - } else if z.Id != nil { - zoneId = *z.Id - } else { - return fmt.Errorf("zone id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), zoneId) - - if z.Primaries == nil { - model.Primaries = types.ListNull(types.StringType) - } else { - respPrimaries := *z.Primaries - modelPrimaries, err := utils.ListValuetoStringSlice(model.Primaries) - if err != nil { - return err - } - - reconciledPrimaries := utils.ReconcileStringSlices(modelPrimaries, respPrimaries) - - primariesTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledPrimaries) - if diags.HasError() { - return fmt.Errorf("failed to map zone primaries: %w", core.DiagsToError(diags)) - } - - model.Primaries = primariesTF - } - model.ZoneId = types.StringValue(zoneId) - model.Description = types.StringPointerValue(z.Description) - model.Acl = types.StringPointerValue(z.Acl) - model.Active = types.BoolPointerValue(z.Active) - model.ContactEmail = types.StringPointerValue(z.ContactEmail) - model.DefaultTTL = types.Int64PointerValue(z.DefaultTTL) - model.DnsName = types.StringPointerValue(z.DnsName) - model.ExpireTime = types.Int64PointerValue(z.ExpireTime) - model.IsReverseZone = types.BoolPointerValue(z.IsReverseZone) - model.Name = types.StringPointerValue(z.Name) - model.NegativeCache = types.Int64PointerValue(z.NegativeCache) - model.PrimaryNameServer = types.StringPointerValue(z.PrimaryNameServer) - model.RecordCount = types.Int64PointerValue(rc) - model.RefreshTime = types.Int64PointerValue(z.RefreshTime) - model.RetryTime = types.Int64PointerValue(z.RetryTime) - model.SerialNumber = types.Int64PointerValue(z.SerialNumber) - model.State = types.StringValue(string(z.GetState())) - model.Type = types.StringValue(string(z.GetType())) - model.Visibility = types.StringValue(string(z.GetVisibility())) - return nil -} - -func toCreatePayload(model *Model) (*dns.CreateZonePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - modelPrimaries := []string{} - for _, primary := range model.Primaries.Elements() { - primaryString, ok := primary.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelPrimaries = append(modelPrimaries, primaryString.ValueString()) - } - return &dns.CreateZonePayload{ - Name: conversion.StringValueToPointer(model.Name), - DnsName: conversion.StringValueToPointer(model.DnsName), - ContactEmail: conversion.StringValueToPointer(model.ContactEmail), - Description: conversion.StringValueToPointer(model.Description), - Acl: conversion.StringValueToPointer(model.Acl), - Type: dns.CreateZonePayloadGetTypeAttributeType(conversion.StringValueToPointer(model.Type)), - DefaultTTL: conversion.Int64ValueToPointer(model.DefaultTTL), - ExpireTime: conversion.Int64ValueToPointer(model.ExpireTime), - RefreshTime: conversion.Int64ValueToPointer(model.RefreshTime), - RetryTime: conversion.Int64ValueToPointer(model.RetryTime), - NegativeCache: conversion.Int64ValueToPointer(model.NegativeCache), - IsReverseZone: conversion.BoolValueToPointer(model.IsReverseZone), - Primaries: &modelPrimaries, - }, nil -} - -func toUpdatePayload(model *Model) (*dns.PartialUpdateZonePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &dns.PartialUpdateZonePayload{ - Name: conversion.StringValueToPointer(model.Name), - ContactEmail: conversion.StringValueToPointer(model.ContactEmail), - Description: conversion.StringValueToPointer(model.Description), - Acl: conversion.StringValueToPointer(model.Acl), - DefaultTTL: conversion.Int64ValueToPointer(model.DefaultTTL), - ExpireTime: conversion.Int64ValueToPointer(model.ExpireTime), - RefreshTime: conversion.Int64ValueToPointer(model.RefreshTime), - RetryTime: conversion.Int64ValueToPointer(model.RetryTime), - NegativeCache: conversion.Int64ValueToPointer(model.NegativeCache), - Primaries: nil, // API returns error if this field is set, even if nothing changes - }, nil -} diff --git a/stackit/internal/services/dns/zone/resource_test.go b/stackit/internal/services/dns/zone/resource_test.go deleted file mode 100644 index d12cd90d..00000000 --- a/stackit/internal/services/dns/zone/resource_test.go +++ /dev/null @@ -1,437 +0,0 @@ -package dns - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *dns.ZoneResponse - expected Model - isValid bool - }{ - { - "default_ok", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - &dns.ZoneResponse{ - Zone: &dns.Zone{ - Id: utils.Ptr("zid"), - }, - }, - Model{ - Id: types.StringValue("pid,zid"), - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - Name: types.StringNull(), - DnsName: types.StringNull(), - Acl: types.StringNull(), - DefaultTTL: types.Int64Null(), - ExpireTime: types.Int64Null(), - RefreshTime: types.Int64Null(), - RetryTime: types.Int64Null(), - SerialNumber: types.Int64Null(), - NegativeCache: types.Int64Null(), - Type: types.StringValue(""), - State: types.StringValue(""), - PrimaryNameServer: types.StringNull(), - Primaries: types.ListNull(types.StringType), - Visibility: types.StringValue(""), - }, - true, - }, - { - "values_ok", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - &dns.ZoneResponse{ - Zone: &dns.Zone{ - Id: utils.Ptr("zid"), - Name: utils.Ptr("name"), - DnsName: utils.Ptr("dnsname"), - Acl: utils.Ptr("acl"), - Active: utils.Ptr(false), - CreationStarted: utils.Ptr("bar"), - CreationFinished: utils.Ptr("foo"), - DefaultTTL: utils.Ptr(int64(1)), - ExpireTime: utils.Ptr(int64(2)), - RefreshTime: utils.Ptr(int64(3)), - RetryTime: utils.Ptr(int64(4)), - SerialNumber: utils.Ptr(int64(5)), - NegativeCache: utils.Ptr(int64(6)), - State: dns.ZONESTATE_CREATING.Ptr(), - Type: dns.ZONETYPE_PRIMARY.Ptr(), - Primaries: &[]string{"primary"}, - PrimaryNameServer: utils.Ptr("pns"), - UpdateStarted: utils.Ptr("ufoo"), - UpdateFinished: utils.Ptr("ubar"), - Visibility: dns.ZONEVISIBILITY_PUBLIC.Ptr(), - Error: utils.Ptr("error"), - ContactEmail: utils.Ptr("a@b.cd"), - Description: utils.Ptr("description"), - IsReverseZone: utils.Ptr(false), - RecordCount: utils.Ptr(int64(3)), - }, - }, - Model{ - Id: types.StringValue("pid,zid"), - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - Name: types.StringValue("name"), - DnsName: types.StringValue("dnsname"), - Acl: types.StringValue("acl"), - Active: types.BoolValue(false), - DefaultTTL: types.Int64Value(1), - ExpireTime: types.Int64Value(2), - RefreshTime: types.Int64Value(3), - RetryTime: types.Int64Value(4), - SerialNumber: types.Int64Value(5), - NegativeCache: types.Int64Value(6), - Type: types.StringValue(string(dns.ZONETYPE_PRIMARY)), - State: types.StringValue(string(dns.ZONESTATE_CREATING)), - PrimaryNameServer: types.StringValue("pns"), - Primaries: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("primary"), - }), - Visibility: types.StringValue(string(dns.ZONEVISIBILITY_PUBLIC)), - ContactEmail: types.StringValue("a@b.cd"), - Description: types.StringValue("description"), - IsReverseZone: types.BoolValue(false), - RecordCount: types.Int64Value(3), - }, - true, - }, - { - "primaries_unordered", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - Primaries: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("primary2"), - types.StringValue("primary1"), - }), - }, - &dns.ZoneResponse{ - Zone: &dns.Zone{ - Id: utils.Ptr("zid"), - Name: utils.Ptr("name"), - DnsName: utils.Ptr("dnsname"), - Acl: utils.Ptr("acl"), - Active: utils.Ptr(false), - CreationStarted: utils.Ptr("bar"), - CreationFinished: utils.Ptr("foo"), - DefaultTTL: utils.Ptr(int64(1)), - ExpireTime: utils.Ptr(int64(2)), - RefreshTime: utils.Ptr(int64(3)), - RetryTime: utils.Ptr(int64(4)), - SerialNumber: utils.Ptr(int64(5)), - NegativeCache: utils.Ptr(int64(6)), - State: dns.ZONESTATE_CREATING.Ptr(), - Type: dns.ZONETYPE_PRIMARY.Ptr(), - Primaries: &[]string{ - "primary1", - "primary2", - }, - PrimaryNameServer: utils.Ptr("pns"), - UpdateStarted: utils.Ptr("ufoo"), - UpdateFinished: utils.Ptr("ubar"), - Visibility: dns.ZONEVISIBILITY_PUBLIC.Ptr(), - Error: utils.Ptr("error"), - ContactEmail: utils.Ptr("a@b.cd"), - Description: utils.Ptr("description"), - IsReverseZone: utils.Ptr(false), - RecordCount: utils.Ptr(int64(3)), - }, - }, - Model{ - Id: types.StringValue("pid,zid"), - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - Name: types.StringValue("name"), - DnsName: types.StringValue("dnsname"), - Acl: types.StringValue("acl"), - Active: types.BoolValue(false), - DefaultTTL: types.Int64Value(1), - ExpireTime: types.Int64Value(2), - RefreshTime: types.Int64Value(3), - RetryTime: types.Int64Value(4), - SerialNumber: types.Int64Value(5), - NegativeCache: types.Int64Value(6), - Type: types.StringValue(string(dns.ZONETYPE_PRIMARY)), - State: types.StringValue(string(dns.ZONESTATE_CREATING)), - PrimaryNameServer: types.StringValue("pns"), - Primaries: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("primary2"), - types.StringValue("primary1"), - }), - Visibility: types.StringValue(string(dns.ZONEVISIBILITY_PUBLIC)), - ContactEmail: types.StringValue("a@b.cd"), - Description: types.StringValue("description"), - IsReverseZone: types.BoolValue(false), - RecordCount: types.Int64Value(3), - }, - true, - }, - { - "nullable_fields_and_int_conversions_ok", - Model{ - Id: types.StringValue("pid,zid"), - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - &dns.ZoneResponse{ - Zone: &dns.Zone{ - Id: utils.Ptr("zid"), - Name: utils.Ptr("name"), - DnsName: utils.Ptr("dnsname"), - Acl: utils.Ptr("acl"), - Active: nil, - CreationStarted: utils.Ptr("bar"), - CreationFinished: utils.Ptr("foo"), - DefaultTTL: utils.Ptr(int64(2123456789)), - ExpireTime: utils.Ptr(int64(-2)), - RefreshTime: utils.Ptr(int64(3)), - RetryTime: utils.Ptr(int64(4)), - SerialNumber: utils.Ptr(int64(5)), - NegativeCache: utils.Ptr(int64(0)), - State: dns.ZONESTATE_CREATING.Ptr(), - Type: dns.ZONETYPE_PRIMARY.Ptr(), - Primaries: nil, - PrimaryNameServer: utils.Ptr("pns"), - UpdateStarted: utils.Ptr("ufoo"), - UpdateFinished: utils.Ptr("ubar"), - Visibility: dns.ZONEVISIBILITY_PUBLIC.Ptr(), - ContactEmail: nil, - Description: nil, - IsReverseZone: nil, - RecordCount: utils.Ptr(int64(-2123456789)), - }, - }, - Model{ - Id: types.StringValue("pid,zid"), - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - Name: types.StringValue("name"), - DnsName: types.StringValue("dnsname"), - Acl: types.StringValue("acl"), - Active: types.BoolNull(), - DefaultTTL: types.Int64Value(2123456789), - ExpireTime: types.Int64Value(-2), - RefreshTime: types.Int64Value(3), - RetryTime: types.Int64Value(4), - SerialNumber: types.Int64Value(5), - NegativeCache: types.Int64Value(0), - Type: types.StringValue(string(dns.ZONETYPE_PRIMARY)), - Primaries: types.ListNull(types.StringType), - State: types.StringValue(string(dns.ZONESTATE_CREATING)), - PrimaryNameServer: types.StringValue("pns"), - Visibility: types.StringValue(string(dns.ZONEVISIBILITY_PUBLIC)), - ContactEmail: types.StringNull(), - Description: types.StringNull(), - IsReverseZone: types.BoolNull(), - RecordCount: types.Int64Value(-2123456789), - }, - true, - }, - { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), - ZoneId: types.StringValue("zid"), - }, - &dns.ZoneResponse{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *dns.CreateZonePayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("Name"), - DnsName: types.StringValue("DnsName"), - }, - &dns.CreateZonePayload{ - Name: utils.Ptr("Name"), - DnsName: utils.Ptr("DnsName"), - Primaries: &[]string{}, - }, - true, - }, - { - "mapping_with_conversions_ok", - &Model{ - Name: types.StringValue("Name"), - DnsName: types.StringValue("DnsName"), - Acl: types.StringValue("Acl"), - Description: types.StringValue("Description"), - Type: types.StringValue(string(dns.CREATEZONEPAYLOADTYPE_PRIMARY)), - ContactEmail: types.StringValue("ContactEmail"), - RetryTime: types.Int64Value(3), - RefreshTime: types.Int64Value(4), - ExpireTime: types.Int64Value(5), - DefaultTTL: types.Int64Value(4534534), - NegativeCache: types.Int64Value(-4534534), - Primaries: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("primary"), - }), - IsReverseZone: types.BoolValue(true), - }, - &dns.CreateZonePayload{ - Name: utils.Ptr("Name"), - DnsName: utils.Ptr("DnsName"), - Acl: utils.Ptr("Acl"), - Description: utils.Ptr("Description"), - Type: dns.CREATEZONEPAYLOADTYPE_PRIMARY.Ptr(), - ContactEmail: utils.Ptr("ContactEmail"), - Primaries: &[]string{"primary"}, - RetryTime: utils.Ptr(int64(3)), - RefreshTime: utils.Ptr(int64(4)), - ExpireTime: utils.Ptr(int64(5)), - DefaultTTL: utils.Ptr(int64(4534534)), - NegativeCache: utils.Ptr(int64(-4534534)), - IsReverseZone: utils.Ptr(true), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToPayloadUpdate(t *testing.T) { - tests := []struct { - description string - input *Model - expected *dns.PartialUpdateZonePayload - isValid bool - }{ - { - "single_field_change_ok", - &Model{ - Name: types.StringValue("Name"), - }, - &dns.PartialUpdateZonePayload{ - Name: utils.Ptr("Name"), - }, - true, - }, - { - "mapping_with_conversions_ok", - &Model{ - Name: types.StringValue("Name"), - DnsName: types.StringValue("DnsName"), - Acl: types.StringValue("Acl"), - Active: types.BoolValue(true), - Description: types.StringValue("Description"), - Type: types.StringValue(string(dns.ZONETYPE_PRIMARY)), - ContactEmail: types.StringValue("ContactEmail"), - PrimaryNameServer: types.StringValue("PrimaryNameServer"), - Primaries: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("Primary"), - }), - RetryTime: types.Int64Value(3), - RefreshTime: types.Int64Value(4), - ExpireTime: types.Int64Value(5), - DefaultTTL: types.Int64Value(4534534), - NegativeCache: types.Int64Value(-4534534), - IsReverseZone: types.BoolValue(true), - }, - &dns.PartialUpdateZonePayload{ - Name: utils.Ptr("Name"), - Acl: utils.Ptr("Acl"), - Description: utils.Ptr("Description"), - ContactEmail: utils.Ptr("ContactEmail"), - RetryTime: utils.Ptr(int64(3)), - RefreshTime: utils.Ptr(int64(4)), - ExpireTime: utils.Ptr(int64(5)), - DefaultTTL: utils.Ptr(int64(4534534)), - NegativeCache: utils.Ptr(int64(-4534534)), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/git/git_acc_test.go b/stackit/internal/services/git/git_acc_test.go deleted file mode 100644 index a4a87d00..00000000 --- a/stackit/internal/services/git/git_acc_test.go +++ /dev/null @@ -1,342 +0,0 @@ -package git - -import ( - "context" - _ "embed" - "fmt" - "maps" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -//go:embed testdata/resource-min.tf -var resourceMin string - -//go:embed testdata/resource-max.tf -var resourceMax string - -var nameMin = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var nameMinUpdated = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var nameMax = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var nameMaxUpdated = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var aclUpdated = "192.168.1.0/32" - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(nameMin), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(nameMax), - "acl": config.StringVariable("192.168.0.0/16"), - "flavor": config.StringVariable("git-100"), -} - -func testConfigVarsMinUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMin)) - maps.Copy(tempConfig, testConfigVarsMin) - // update git instance to a new name - // should trigger creating a new instance - tempConfig["name"] = config.StringVariable(nameMinUpdated) - return tempConfig -} - -func testConfigVarsMaxUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMax)) - maps.Copy(tempConfig, testConfigVarsMax) - // update git instance to a new name - // should trigger creating a new instance - tempConfig["name"] = config.StringVariable(nameMaxUpdated) - tempConfig["acl"] = config.StringVariable(aclUpdated) - - return tempConfig -} - -func TestAccGitMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckGitInstanceDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMin, - Config: testutil.GitProviderConfig() + resourceMin, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_git.git", "url"), - resource.TestCheckResourceAttrSet("stackit_git.git", "version"), - resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), - resource.TestCheckResourceAttrSet("stackit_git.git", "created"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), - resource.TestCheckResourceAttrSet("stackit_git.git", "flavor"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMin, - Config: fmt.Sprintf(` - %s - - data "stackit_git" "git" { - project_id = stackit_git.git.project_id - instance_id = stackit_git.git.instance_id - } - `, testutil.GitProviderConfig()+resourceMin, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "project_id", - "data.stackit_git.git", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "instance_id", - "data.stackit_git.git", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "name", - "data.stackit_git.git", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "url", - "data.stackit_git.git", "url", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "version", - "data.stackit_git.git", "version", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "created", - "data.stackit_git.git", "created", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "consumed_object_storage", - "data.stackit_git.git", "consumed_object_storage", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "consumed_disk", - "data.stackit_git.git", "consumed_disk", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "flavor", - "data.stackit_git.git", "flavor", - ), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_git.git", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_git.git"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_git.git") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigVarsMinUpdated(), - Config: testutil.GitProviderConfig() + resourceMin, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMinUpdated()["name"])), - resource.TestCheckResourceAttrSet("stackit_git.git", "url"), - resource.TestCheckResourceAttrSet("stackit_git.git", "version"), - resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), - resource.TestCheckResourceAttrSet("stackit_git.git", "created"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), - resource.TestCheckResourceAttrSet("stackit_git.git", "flavor"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccGitMax(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckGitInstanceDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMax, - Config: testutil.GitProviderConfig() + resourceMax, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_git.git", "flavor", testutil.ConvertConfigVariable(testConfigVarsMax["flavor"])), - resource.TestCheckResourceAttr("stackit_git.git", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), - resource.TestCheckResourceAttrSet("stackit_git.git", "url"), - resource.TestCheckResourceAttrSet("stackit_git.git", "version"), - resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), - resource.TestCheckResourceAttrSet("stackit_git.git", "created"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMax, - Config: fmt.Sprintf(` - %s - - data "stackit_git" "git" { - project_id = stackit_git.git.project_id - instance_id = stackit_git.git.instance_id - } - `, testutil.GitProviderConfig()+resourceMax, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "project_id", - "data.stackit_git.git", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "instance_id", - "data.stackit_git.git", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "name", - "data.stackit_git.git", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "url", - "data.stackit_git.git", "url", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "version", - "data.stackit_git.git", "version", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "created", - "data.stackit_git.git", "created", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "consumed_object_storage", - "data.stackit_git.git", "consumed_object_storage", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "consumed_disk", - "data.stackit_git.git", "consumed_disk", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "flavor", - "data.stackit_git.git", "flavor", - ), - resource.TestCheckResourceAttrPair( - "stackit_git.git", "acl", - "data.stackit_git.git", "acl", - ), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_git.git", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_git.git"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_git.git") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigVarsMaxUpdated(), - Config: testutil.GitProviderConfig() + resourceMax, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_git.git", "project_id", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_git.git", "name", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_git.git", "flavor", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["flavor"])), - resource.TestCheckResourceAttr("stackit_git.git", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMaxUpdated()["acl"])), - resource.TestCheckResourceAttrSet("stackit_git.git", "url"), - resource.TestCheckResourceAttrSet("stackit_git.git", "version"), - resource.TestCheckResourceAttrSet("stackit_git.git", "instance_id"), - resource.TestCheckResourceAttrSet("stackit_git.git", "created"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_object_storage"), - resource.TestCheckResourceAttrSet("stackit_git.git", "consumed_disk"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckGitInstanceDestroy(s *terraform.State) error { - ctx := context.Background() - var client *git.APIClient - var err error - - if testutil.GitCustomEndpoint == "" { - client, err = git.NewAPIClient() - } else { - client, err = git.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.GitCustomEndpoint), - ) - } - - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var instancesToDestroy []string - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_git" { - continue - } - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting git instances: %w", err) - } - - gitInstances := *instancesResp.Instances - for i := range gitInstances { - if gitInstances[i].Id == nil { - continue - } - if utils.Contains(instancesToDestroy, *gitInstances[i].Id) { - err := client.DeleteInstance(ctx, testutil.ProjectId, *gitInstances[i].Id).Execute() - if err != nil { - return fmt.Errorf("destroying git instance %s during CheckDestroy: %w", *gitInstances[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/git/instance/datasource.go b/stackit/internal/services/git/instance/datasource.go deleted file mode 100644 index 65aad54b..00000000 --- a/stackit/internal/services/git/instance/datasource.go +++ /dev/null @@ -1,166 +0,0 @@ -package instance - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - gitUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/git" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &gitDataSource{} -) - -// NewGitDataSource creates a new instance of the gitDataSource. -func NewGitDataSource() datasource.DataSource { - return &gitDataSource{} -} - -// gitDataSource is the datasource implementation. -type gitDataSource struct { - client *git.APIClient -} - -// Configure sets up the API client for the git instance resource. -func (g *gitDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_git", "datasource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := gitUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - g.client = apiClient - tflog.Info(ctx, "git client configured") -} - -// Metadata provides metadata for the git datasource. -func (g *gitDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_git" -} - -// Schema defines the schema for the git data source. -func (g *gitDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("Git Instance datasource schema.", core.Datasource), - Description: "Git Instance datasource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - Computed: true, - ElementType: types.StringType, - }, - "consumed_disk": schema.StringAttribute{ - Description: descriptions["consumed_disk"], - Computed: true, - }, - "consumed_object_storage": schema.StringAttribute{ - Description: descriptions["consumed_object_storage"], - Computed: true, - }, - "created": schema.StringAttribute{ - Description: descriptions["created"], - Computed: true, - }, - "flavor": schema.StringAttribute{ - Description: descriptions["flavor"], - Computed: true, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "url": schema.StringAttribute{ - Description: descriptions["url"], - Computed: true, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Computed: true, - }, - }, - } -} - -func (g *gitDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID and instance id of the model - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - - // Read the current git instance via id - gitInstanceResp, err := g.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, gitInstanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - tflog.Info(ctx, fmt.Sprintf("read git instance %s", instanceId)) -} diff --git a/stackit/internal/services/git/instance/resource.go b/stackit/internal/services/git/instance/resource.go deleted file mode 100644 index 5811e5e3..00000000 --- a/stackit/internal/services/git/instance/resource.go +++ /dev/null @@ -1,425 +0,0 @@ -package instance - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" - "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/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/git" - "github.com/stackitcloud/stackit-sdk-go/services/git/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - gitUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &gitResource{} - _ resource.ResourceWithConfigure = &gitResource{} - _ resource.ResourceWithImportState = &gitResource{} -) - -// Model represents the schema for the git resource. -type Model struct { - Id types.String `tfsdk:"id"` // Required by Terraform - ACL types.List `tfsdk:"acl"` - ConsumedDisk types.String `tfsdk:"consumed_disk"` - ConsumedObjectStorage types.String `tfsdk:"consumed_object_storage"` - Created types.String `tfsdk:"created"` - Flavor types.String `tfsdk:"flavor"` - InstanceId types.String `tfsdk:"instance_id"` - Name types.String `tfsdk:"name"` - ProjectId types.String `tfsdk:"project_id"` - Url types.String `tfsdk:"url"` - Version types.String `tfsdk:"version"` -} - -// NewGitResource is a helper function to create a new git resource instance. -func NewGitResource() resource.Resource { - return &gitResource{} -} - -// gitResource implements the resource interface for git instances. -type gitResource struct { - client *git.APIClient -} - -// descriptions for the attributes in the Schema -var descriptions = map[string]string{ - "id": "Terraform's internal resource ID, structured as \"`project_id`,`instance_id`\".", - "acl": "Restricted ACL for instance access.", - "consumed_disk": "How many bytes of disk space is consumed.", - "consumed_object_storage": "How many bytes of Object Storage is consumed.", - "created": "Instance creation timestamp in RFC3339 format.", - "flavor": "Instance flavor. If not provided, defaults to git-100. For a list of available flavors, refer to our API documentation: `https://docs.api.stackit.cloud/documentation/git/version/v1beta`", - "instance_id": "ID linked to the git instance.", - "name": "Unique name linked to the git instance.", - "project_id": "STACKIT project ID to which the git instance is associated.", - "url": "Url linked to the git instance.", - "version": "Version linked to the git instance.", -} - -// Configure sets up the API client for the git instance resource. -func (g *gitResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_git", "resource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := gitUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - g.client = apiClient - tflog.Info(ctx, "git client configured") -} - -// Metadata sets the resource type name for the git instance resource. -func (g *gitResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_git" -} - -// Schema defines the schema for the resource. -func (g *gitResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - MarkdownDescription: fmt.Sprintf( - "%s %s", - features.AddBetaDescription("Git Instance resource schema.", core.Resource), - "This resource currently does not support updates. Changing the ACLs, flavor, or name will trigger resource recreation. Update functionality will be added soon. In the meantime, please proceed with caution. To update these attributes, please open a support ticket.", - ), - Description: "Git Instance resource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "consumed_disk": schema.StringAttribute{ - Description: descriptions["consumed_disk"], - Computed: true, - }, - "consumed_object_storage": schema.StringAttribute{ - Description: descriptions["consumed_object_storage"], - Computed: true, - }, - "created": schema.StringAttribute{ - Description: descriptions["created"], - Computed: true, - }, - "flavor": schema.StringAttribute{ - Description: descriptions["flavor"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Optional: true, - Computed: true, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthBetween(5, 32), - }, - }, - "url": schema.StringAttribute{ - Description: descriptions["url"], - Computed: true, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state for the git instance. -func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the planned values for the resource. - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Set logging context with the project ID and instance ID. - projectId := model.ProjectId.ValueString() - instanceName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_name", instanceName) - - payload, diags := toCreatePayload(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Create the new git instance via the API client. - gitInstanceResp, err := g.client.CreateInstance(ctx, projectId). - CreateInstancePayload(payload). - Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - gitInstanceId := *gitInstanceResp.Id - _, err = wait.CreateGitInstanceWaitHandler(ctx, g.client, projectId, gitInstanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Git instance creation waiting: %v", err)) - return - } - - err = mapFields(ctx, gitInstanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Mapping fields: %v", err)) - return - } - - // Set the state with fully populated data. - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Git Instance created") -} - -// Read refreshes the Terraform state with the latest git instance data. -func (g *gitResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID and instance id of the model - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - - // Read the current git instance via id - gitInstanceResp, err := g.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, gitInstanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - tflog.Info(ctx, fmt.Sprintf("read git instance %s", instanceId)) -} - -// Update attempts to update the resource. In this case, git instances cannot be updated. -// Note: This method is intentionally left without update logic because changes -// to 'project_id' or 'name' require the resource to be entirely replaced. -// As a result, the Update function is redundant since any modifications will -// automatically trigger a resource recreation through Terraform's built-in -// lifecycle management. -func (g *gitResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // git instances cannot be updated, so we log an error. - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating git instance", "Git Instance can't be updated") -} - -// Delete deletes the git instance and removes it from the Terraform state on success. -func (g *gitResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Call API to delete the existing git instance. - err := g.client.DeleteInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting git instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteGitInstanceWaitHandler(ctx, g.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for instance deletion", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Git instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (g *gitResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - // Split the import identifier to extract project ID and email. - idParts := strings.Split(req.ID, core.Separator) - - // Ensure the import identifier format is correct. - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing git instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - projectId := idParts[0] - instanceId := idParts[1] - - // Set the project ID and instance ID attributes in the state. - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId)...) - tflog.Info(ctx, "Git instance state imported") -} - -// mapFields maps a Git response to the model. -func mapFields(ctx context.Context, resp *git.Instance, model *Model) error { - if resp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if resp.Id == nil { - return fmt.Errorf("git instance id not present") - } - - aclList := types.ListNull(types.StringType) - var diags diag.Diagnostics - if resp.Acl != nil && len(*resp.Acl) > 0 { - aclList, diags = types.ListValueFrom(ctx, types.StringType, resp.Acl) - if diags.HasError() { - return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) - } - } - - model.Created = types.StringNull() - if resp.Created != nil && resp.Created.String() != "" { - model.Created = types.StringValue(resp.Created.String()) - } - - // Build the ID by combining the project ID and instance id and assign the model's fields. - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), *resp.Id) - model.ACL = aclList - model.ConsumedDisk = types.StringPointerValue(resp.ConsumedDisk) - model.ConsumedObjectStorage = types.StringPointerValue(resp.ConsumedObjectStorage) - model.Flavor = types.StringPointerValue(resp.Flavor) - model.InstanceId = types.StringPointerValue(resp.Id) - model.Name = types.StringPointerValue(resp.Name) - model.Url = types.StringPointerValue(resp.Url) - model.Version = types.StringPointerValue(resp.Version) - - return nil -} - -// toCreatePayload creates the payload to create a git instance -func toCreatePayload(ctx context.Context, model *Model) (git.CreateInstancePayload, diag.Diagnostics) { - diags := diag.Diagnostics{} - - if model == nil { - return git.CreateInstancePayload{}, diags - } - - payload := git.CreateInstancePayload{ - Name: model.Name.ValueStringPointer(), - } - - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - var acl []string - aclDiags := model.ACL.ElementsAs(ctx, &acl, false) - diags.Append(aclDiags...) - if !aclDiags.HasError() { - payload.Acl = &acl - } - } - - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - payload.Flavor = git.CreateInstancePayloadGetFlavorAttributeType(model.Flavor.ValueStringPointer()) - } - - return payload, diags -} diff --git a/stackit/internal/services/git/instance/resource_test.go b/stackit/internal/services/git/instance/resource_test.go deleted file mode 100644 index 585f21ed..00000000 --- a/stackit/internal/services/git/instance/resource_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package instance - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" -) - -var ( - testInstanceId = uuid.New().String() - testProjectId = uuid.New().String() -) - -func TestMapFields(t *testing.T) { - createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC") - if err != nil { - t.Fatalf("failed to parse test time: %v", err) - } - - tests := []struct { - description string - input *git.Instance - expected *Model - isValid bool - }{ - { - description: "minimal_input_name_only", - input: &git.Instance{ - Id: utils.Ptr(testInstanceId), - Name: utils.Ptr("git-min-instance"), - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testInstanceId)), - ProjectId: types.StringValue(testProjectId), - InstanceId: types.StringValue(testInstanceId), - Name: types.StringValue("git-min-instance"), - ACL: types.ListNull(types.StringType), - Flavor: types.StringNull(), - Url: types.StringNull(), - Version: types.StringNull(), - Created: types.StringNull(), - ConsumedDisk: types.StringNull(), - ConsumedObjectStorage: types.StringNull(), - }, - isValid: true, - }, - { - description: "full_input_with_acl_and_flavor", - input: &git.Instance{ - Acl: &[]string{"192.168.0.0/24"}, - ConsumedDisk: utils.Ptr("1.00 GB"), - ConsumedObjectStorage: utils.Ptr("2.00 GB"), - Created: &createdTime, - Flavor: utils.Ptr("git-100"), - Id: utils.Ptr(testInstanceId), - Name: utils.Ptr("git-full-instance"), - Url: utils.Ptr("https://git-full-instance.git.onstackit.cloud"), - Version: utils.Ptr("v1.9.1"), - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testInstanceId)), - ProjectId: types.StringValue(testProjectId), - InstanceId: types.StringValue(testInstanceId), - Name: types.StringValue("git-full-instance"), - ACL: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("192.168.0.0/24")}), - Flavor: types.StringValue("git-100"), - Url: types.StringValue("https://git-full-instance.git.onstackit.cloud"), - Version: types.StringValue("v1.9.1"), - Created: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - ConsumedDisk: types.StringValue("1.00 GB"), - ConsumedObjectStorage: types.StringValue("2.00 GB"), - }, - isValid: true, - }, - { - description: "empty_acls", - input: &git.Instance{ - Id: utils.Ptr(testInstanceId), - Name: utils.Ptr("git-empty-acl"), - Acl: &[]string{}, - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s", testProjectId, testInstanceId)), - ProjectId: types.StringValue(testProjectId), - InstanceId: types.StringValue(testInstanceId), - Name: types.StringValue("git-empty-acl"), - ACL: types.ListNull(types.StringType), - Flavor: types.StringNull(), - Url: types.StringNull(), - Version: types.StringNull(), - Created: types.StringNull(), - ConsumedDisk: types.StringNull(), - ConsumedObjectStorage: types.StringNull(), - }, - isValid: true, - }, - { - description: "nil_instance", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "empty_instance", - input: &git.Instance{}, - expected: nil, - isValid: false, - }, - { - description: "missing_id", - input: &git.Instance{ - Name: utils.Ptr("git-missing-id"), - }, - expected: nil, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{} - if tt.expected != nil { - state.ProjectId = tt.expected.ProjectId - } - err := mapFields(context.Background(), tt.input, state) - - if tt.isValid && err != nil { - t.Fatalf("expected success, got error: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("expected error, got nil") - } - if tt.isValid { - if diff := cmp.Diff(tt.expected, state); diff != "" { - t.Errorf("unexpected diff (-want +got):\n%s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected git.CreateInstancePayload - expectError bool - }{ - { - description: "default values", - input: &Model{ - Name: types.StringValue("example-instance"), - Flavor: types.StringNull(), - ACL: types.ListNull(types.StringType), - }, - expected: git.CreateInstancePayload{ - Name: utils.Ptr("example-instance"), - }, - expectError: false, - }, - { - description: "simple values with ACL and Flavor", - input: &Model{ - Name: types.StringValue("my-instance"), - Flavor: types.StringValue("git-100"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("10.0.0.1"), - types.StringValue("10.0.0.2"), - }), - }, - expected: git.CreateInstancePayload{ - Name: utils.Ptr("my-instance"), - Flavor: git.CREATEINSTANCEPAYLOADFLAVOR__100.Ptr(), - Acl: &[]string{"10.0.0.1", "10.0.0.2"}, - }, - expectError: false, - }, - { - description: "empty ACL still valid", - input: &Model{ - Name: types.StringValue("my-instance"), - Flavor: types.StringValue("git-100"), - ACL: types.ListValueMust(types.StringType, []attr.Value{}), - }, - expected: git.CreateInstancePayload{ - Name: utils.Ptr("my-instance"), - Flavor: git.CREATEINSTANCEPAYLOADFLAVOR__100.Ptr(), - Acl: &[]string{}, - }, - expectError: false, - }, - { - description: "nil input model", - input: nil, - expected: git.CreateInstancePayload{}, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, diags := toCreatePayload(context.Background(), tt.input) - - if tt.expectError && !diags.HasError() { - t.Fatalf("expected diagnostics error but got none") - } - - if !tt.expectError && diags.HasError() { - t.Fatalf("unexpected diagnostics error: %v", diags) - } - - if diff := cmp.Diff(tt.expected, output); diff != "" { - t.Fatalf("unexpected payload (-want +got):\n%s", diff) - } - }) - } -} diff --git a/stackit/internal/services/git/testdata/resource-max.tf b/stackit/internal/services/git/testdata/resource-max.tf deleted file mode 100644 index 3945d77b..00000000 --- a/stackit/internal/services/git/testdata/resource-max.tf +++ /dev/null @@ -1,14 +0,0 @@ - -variable "project_id" {} -variable "name" {} -variable "acl" {} -variable "flavor" {} - -resource "stackit_git" "git" { - project_id = var.project_id - name = var.name - acl = [ - var.acl - ] - flavor = var.flavor -} diff --git a/stackit/internal/services/git/testdata/resource-min.tf b/stackit/internal/services/git/testdata/resource-min.tf deleted file mode 100644 index e412a1c9..00000000 --- a/stackit/internal/services/git/testdata/resource-min.tf +++ /dev/null @@ -1,8 +0,0 @@ - -variable "project_id" {} -variable "name" {} - -resource "stackit_git" "git" { - project_id = var.project_id - name = var.name -} diff --git a/stackit/internal/services/git/utils/util.go b/stackit/internal/services/git/utils/util.go deleted file mode 100644 index e55f2351..00000000 --- a/stackit/internal/services/git/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/git" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *git.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.GitCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.GitCustomEndpoint)) - } - apiClient, err := git.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/git/utils/util_test.go b/stackit/internal/services/git/utils/util_test.go deleted file mode 100644 index 92c81202..00000000 --- a/stackit/internal/services/git/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/git" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://git-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *git.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *git.APIClient { - apiClient, err := git.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - GitCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *git.APIClient { - apiClient, err := git.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/iaas/affinitygroup/const.go b/stackit/internal/services/iaas/affinitygroup/const.go deleted file mode 100644 index 2d16175f..00000000 --- a/stackit/internal/services/iaas/affinitygroup/const.go +++ /dev/null @@ -1,41 +0,0 @@ -package affinitygroup - -const exampleUsageWithServer = ` - -### Usage with server` + "\n" + - "```terraform" + ` -resource "stackit_affinity_group" "affinity-group" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-key-pair" - policy = "soft-affinity" -} - -resource "stackit_server" "example-server" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-server" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - affinity_group = stackit_affinity_group.affinity-group.affinity_group_id - availability_zone = "eu01-1" - machine_type = "g2i.1" -} -` + "\n```" - -const policies = ` - -### Policies - -* ` + "`hard-affinity`" + `- All servers launched in this group will be hosted on the same compute node. - -* ` + "`hard-anti-affinity`" + `- All servers launched in this group will be - hosted on different compute nodes. - -* ` + "`soft-affinity`" + `- All servers launched in this group will be hosted - on the same compute node if possible, but if not possible they still will be scheduled instead of failure. - -* ` + "`soft-anti-affinity`" + `- All servers launched in this group will be hosted on different compute nodes if possible, - but if not possible they still will be scheduled instead of failure. -` diff --git a/stackit/internal/services/iaas/affinitygroup/datasource.go b/stackit/internal/services/iaas/affinitygroup/datasource.go deleted file mode 100644 index 4937f9c6..00000000 --- a/stackit/internal/services/iaas/affinitygroup/datasource.go +++ /dev/null @@ -1,165 +0,0 @@ -package affinitygroup - -import ( - "context" - "fmt" - "net/http" - "regexp" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -var ( - _ datasource.DataSource = &affinityGroupDatasource{} - _ datasource.DataSourceWithConfigure = &affinityGroupDatasource{} -) - -func NewAffinityGroupDatasource() datasource.DataSource { - return &affinityGroupDatasource{} -} - -type affinityGroupDatasource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -func (d *affinityGroupDatasource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -func (d *affinityGroupDatasource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_affinity_group" -} - -func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptionMain := "Affinity Group schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: descriptionMain, - MarkdownDescription: descriptionMain, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID to which the affinity group is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "affinity_group_id": schema.StringAttribute{ - Description: "The affinity group ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the affinity group.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "policy": schema.StringAttribute{ - Description: "The policy of the affinity group.", - Computed: true, - }, - "members": schema.ListAttribute{ - Description: descriptionMain, - Computed: true, - ElementType: types.StringType, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - validate.UUID(), - ), - }, - }, - }, - } -} - -func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - affinityGroupId := model.AffinityGroupId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId) - - affinityGroupResp, err := d.client.GetAffinityGroupExecute(ctx, projectId, region, affinityGroupId) - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading affinity group", - fmt.Sprintf("Affinity group with ID %q does not exist in project %q.", affinityGroupId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, affinityGroupResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err)) - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Affinity group read") -} diff --git a/stackit/internal/services/iaas/affinitygroup/resource.go b/stackit/internal/services/iaas/affinitygroup/resource.go deleted file mode 100644 index 6597ff65..00000000 --- a/stackit/internal/services/iaas/affinitygroup/resource.go +++ /dev/null @@ -1,388 +0,0 @@ -package affinitygroup - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -var ( - _ resource.Resource = &affinityGroupResource{} - _ resource.ResourceWithConfigure = &affinityGroupResource{} - _ resource.ResourceWithImportState = &affinityGroupResource{} - _ resource.ResourceWithModifyPlan = &affinityGroupResource{} -) - -// Model is the provider's internal model -type Model struct { - Id types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - AffinityGroupId types.String `tfsdk:"affinity_group_id"` - Name types.String `tfsdk:"name"` - Policy types.String `tfsdk:"policy"` - Members types.List `tfsdk:"members"` -} - -func NewAffinityGroupResource() resource.Resource { - return &affinityGroupResource{} -} - -// affinityGroupResource is the resource implementation. -type affinityGroupResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *affinityGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_affinity_group" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *affinityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *affinityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Affinity Group schema." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: description + "\n\n" + exampleUsageWithServer + policies, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`affinity_group_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID to which the affinity group is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "affinity_group_id": schema.StringAttribute{ - Description: "The affinity group ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the affinity group.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "policy": schema.StringAttribute{ - Description: "The policy of the affinity group.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{}, - }, - "members": schema.ListAttribute{ - Description: "The servers that are part of the affinity group.", - Computed: true, - ElementType: types.StringType, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - validate.UUID(), - ), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - ctx = core.InitProviderContext(ctx) - - // Create new affinityGroup - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Creating API payload: %v", err)) - return - } - affinityGroupResp, err := r.client.CreateAffinityGroup(ctx, projectId, region).CreateAffinityGroupPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupResp.Id) - - // Map response body to schema - err = mapFields(ctx, affinityGroupResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Affinity group created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - affinityGroupId := model.AffinityGroupId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId) - - affinityGroupResp, err := r.client.GetAffinityGroupExecute(ctx, projectId, region, affinityGroupId) - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Call API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, affinityGroupResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Processing API payload: %v", err)) - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Affinity group read") -} - -func (r *affinityGroupResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update is not supported, all fields require replace -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *affinityGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - affinityGroupId := model.AffinityGroupId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupId) - - // Delete existing affinity group - err := r.client.DeleteAffinityGroupExecute(ctx, projectId, region, affinityGroupId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting affinity group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Affinity group deleted") -} - -func (r *affinityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing affinity group", - fmt.Sprintf("Expected import indentifier with format: [project_id],[region],[affinity_group_id], got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "affinity_group_id": idParts[2], - }) - - tflog.Info(ctx, "affinity group state imported") -} - -func toCreatePayload(model *Model) (*iaas.CreateAffinityGroupPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - nameValue := conversion.StringValueToPointer(model.Name) - policyValue := conversion.StringValueToPointer(model.Policy) - - return &iaas.CreateAffinityGroupPayload{ - Name: nameValue, - Policy: policyValue, - }, nil -} - -func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model *Model, region string) error { - if affinityGroupResp == nil { - return fmt.Errorf("response input is nil") - } - - if model == nil { - return fmt.Errorf("nil model") - } - - var affinityGroupId string - if model.AffinityGroupId.ValueString() != "" { - affinityGroupId = model.AffinityGroupId.ValueString() - } else if affinityGroupResp.Id != nil { - affinityGroupId = *affinityGroupResp.Id - } else { - return fmt.Errorf("affinity group id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, affinityGroupId) - model.Region = types.StringValue(region) - - if affinityGroupResp.Members != nil && len(*affinityGroupResp.Members) > 0 { - members, diags := types.ListValueFrom(ctx, types.StringType, *affinityGroupResp.Members) - if diags.HasError() { - return fmt.Errorf("convert members to StringValue list: %w", core.DiagsToError(diags)) - } - model.Members = members - } else if model.Members.IsNull() { - model.Members = types.ListNull(types.StringType) - } - - model.AffinityGroupId = types.StringValue(affinityGroupId) - - model.Name = types.StringPointerValue(affinityGroupResp.Name) - model.Policy = types.StringPointerValue(affinityGroupResp.Policy) - - return nil -} diff --git a/stackit/internal/services/iaas/affinitygroup/resource_test.go b/stackit/internal/services/iaas/affinitygroup/resource_test.go deleted file mode 100644 index 26f4bc05..00000000 --- a/stackit/internal/services/iaas/affinitygroup/resource_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package affinitygroup - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.AffinityGroup - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - AffinityGroupId: types.StringValue("aid"), - }, - input: &iaas.AffinityGroup{ - Id: utils.Ptr("aid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,aid"), - ProjectId: types.StringValue("pid"), - AffinityGroupId: types.StringValue("aid"), - Name: types.StringNull(), - Policy: types.StringNull(), - Members: types.ListNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_affinity_group_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.AffinityGroup{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed") - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %v", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateAffinityGroupPayload - isValid bool - }{ - { - "default", - &Model{ - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - Policy: types.StringValue("policy"), - }, - &iaas.CreateAffinityGroupPayload{ - Name: utils.Ptr("name"), - Policy: utils.Ptr("policy"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go deleted file mode 100644 index a44aa75a..00000000 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ /dev/null @@ -1,4842 +0,0 @@ -package iaas_test - -import ( - "context" - _ "embed" - "errors" - "fmt" - "maps" - "net/http" - "os" - "path/filepath" - "strings" - "sync" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/plancheck" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - waitAlpha "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/resource-security-group-min.tf - resourceSecurityGroupMinConfig string - - //go:embed testdata/resource-security-group-max.tf - resourceSecurityGroupMaxConfig string - - //go:embed testdata/datasource-image-v2-variants.tf - dataSourceImageVariants string - - //go:embed testdata/datasource-public-ip-ranges.tf - datasourcePublicIpRanges string - - //go:embed testdata/resource-image-min.tf - resourceImageMinConfig string - - //go:embed testdata/resource-image-max.tf - resourceImageMaxConfig string - - //go:embed testdata/resource-key-pair-min.tf - resourceKeyPairMinConfig string - - //go:embed testdata/resource-key-pair-max.tf - resourceKeyPairMaxConfig string - - //go:embed testdata/resource-network-area-min.tf - resourceNetworkAreaMinConfig string - - //go:embed testdata/resource-network-area-max.tf - resourceNetworkAreaMaxConfig string - - //go:embed testdata/resource-network-area-region-min.tf - resourceNetworkAreaRegionMinConfig string - - //go:embed testdata/resource-network-area-region-max.tf - resourceNetworkAreaRegionMaxConfig string - - //go:embed testdata/resource-network-min.tf - resourceNetworkMinConfig string - - //go:embed testdata/resource-network-max.tf - resourceNetworkMaxConfig string - - //go:embed testdata/resource-network-interface-min.tf - resourceNetworkInterfaceMinConfig string - - //go:embed testdata/resource-network-interface-max.tf - resourceNetworkInterfaceMaxConfig string - - //go:embed testdata/resource-volume-min.tf - resourceVolumeMinConfig string - - //go:embed testdata/resource-volume-max.tf - resourceVolumeMaxConfig string - - //go:embed testdata/resource-affinity-group-min.tf - resourceAffinityGroupMinConfig string - - //go:embed testdata/resource-server-min.tf - resourceServerMinConfig string - - //go:embed testdata/resource-server-max.tf - resourceServerMaxConfig string - - //go:embed testdata/resource-server-max-server-attachments.tf - resourceServerMaxAttachmentConfig string - - //go:embed testdata/datasource-machinetype.tf - dataSourceMachineTypeConfig string -) - -const ( - keypairPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDsPd27M449akqCtdFg2+AmRVJz6eWio0oMP9dVg7XZ" -) - -// SERVER - MIN - -var testConfigServerVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "network_name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "machine_type": config.StringVariable("t1.1"), - "image_id": config.StringVariable("a2c127b2-b1b5-4aee-986f-41cd11b41279"), -} - -var testConfigServerVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigServerVarsMin { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(testutil.ProjectId) - updatedConfig["machine_type"] = config.StringVariable("t1.2") - return updatedConfig -}() - -// SERVER - MAX - -var testConfigServerVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "name_not_updated": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "machine_type": config.StringVariable("t1.1"), - "image_id": config.StringVariable("a2c127b2-b1b5-4aee-986f-41cd11b41279"), - "availability_zone": config.StringVariable("eu01-1"), - "label": config.StringVariable("label"), - "user_data": config.StringVariable("#!/bin/bash"), - "policy": config.StringVariable("soft-affinity"), - "size": config.IntegerVariable(16), - "service_account_mail": config.StringVariable(testutil.TestProjectServiceAccountEmail), - "public_key": config.StringVariable(keypairPublicKey), - "desired_status": config.StringVariable("active"), -} - -var testConfigServerVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigServerVarsMax { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(testutil.ProjectId) - updatedConfig["machine_type"] = config.StringVariable("t1.2") - updatedConfig["label"] = config.StringVariable("updated") - updatedConfig["desired_status"] = config.StringVariable("inactive") - return updatedConfig -}() - -var testConfigServerVarsMaxUpdatedDesiredStatus = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigServerVarsMaxUpdated { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(testutil.ProjectId) - updatedConfig["machine_type"] = config.StringVariable("t1.2") - updatedConfig["label"] = config.StringVariable("updated") - updatedConfig["desired_status"] = config.StringVariable("deallocated") - return updatedConfig -}() - -// AFFINITY GROUP - MIN - -var testConfigAffinityGroupVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "policy": config.StringVariable("hard-affinity"), -} - -// NETWORK INTERFACE - MIN - -var testConfigNetworkInterfaceVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), -} - -// NETWORK INTERFACE - MAX - -var testConfigNetworkInterfaceVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "allowed_address": config.StringVariable("10.2.10.0/24"), - "ipv4": config.StringVariable("10.2.10.20"), - "ipv4_prefix": config.StringVariable("10.2.10.0/24"), - "security": config.BoolVariable(true), - "label": config.StringVariable("label"), -} - -var testConfigNetworkInterfaceVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigNetworkInterfaceVarsMax { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"]))) - updatedConfig["ipv4"] = config.StringVariable("10.2.10.21") - updatedConfig["security"] = config.BoolVariable(false) - updatedConfig["label"] = config.StringVariable("updated") - return updatedConfig -}() - -// VOLUME - MIN - -var testConfigVolumeVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "availability_zone": config.StringVariable("eu01-1"), - "size": config.IntegerVariable(16), -} - -var testConfigVolumeVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigVolumeVarsMin { - updatedConfig[k] = v - } - updatedConfig["size"] = config.IntegerVariable(20) - return updatedConfig -}() - -// VOLUME - MAX - -var testConfigVolumeVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "availability_zone": config.StringVariable("eu01-1"), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "size": config.IntegerVariable(16), - "description": config.StringVariable("description"), - "performance_class": config.StringVariable("storage_premium_perf0"), - "label": config.StringVariable("label"), -} - -var testConfigVolumeVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigVolumeVarsMax { - updatedConfig[k] = v - } - updatedConfig["size"] = config.IntegerVariable(20) - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["name"]))) - updatedConfig["description"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"]))) - updatedConfig["label"] = config.StringVariable("updated") - return updatedConfig -}() - -// NETWORK - MIN - -var testConfigNetworkVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), -} - -var testConfigNetworkVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigNetworkVarsMin) - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - return updatedConfig -}() - -// NETWORK - MAX - -var testConfigNetworkVarsMax = config.Variables{ - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "ipv4_gateway": config.StringVariable("10.2.2.1"), - "ipv4_nameserver_0": config.StringVariable("10.2.2.2"), - "ipv4_nameserver_1": config.StringVariable("10.2.2.3"), - "ipv4_prefix": config.StringVariable("10.2.2.0/24"), - "ipv4_prefix_length": config.IntegerVariable(24), - "routed": config.BoolVariable(true), - "label": config.StringVariable("label"), - "organization_id": config.StringVariable(testutil.OrganizationId), - "service_account_mail": config.StringVariable(testutil.TestProjectServiceAccountEmail), -} - -var testConfigNetworkVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigNetworkVarsMax) - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["ipv4_gateway"] = config.StringVariable("") - updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") - updatedConfig["label"] = config.StringVariable("updated") - return updatedConfig -}() - -// NETWORK AREA - MIN - -var testConfigNetworkAreaVarsMin = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), -} - -var testConfigNetworkAreaVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigNetworkAreaVarsMin { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - return updatedConfig -}() - -// NETWORK AREA - MAX - -var testConfigNetworkAreaVarsMax = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "transfer_network": config.StringVariable("10.1.2.0/24"), - "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), - "default_nameservers": config.StringVariable("1.1.1.1"), - "default_prefix_length": config.IntegerVariable(24), - "max_prefix_length": config.IntegerVariable(24), - "min_prefix_length": config.IntegerVariable(16), - "route_destination_type": config.StringVariable("cidrv4"), - "route_destination_value": config.StringVariable("1.1.1.0/24"), - "route_next_hop_type": config.StringVariable("ipv4"), - "route_next_hop_value": config.StringVariable("1.1.1.1"), - "label": config.StringVariable("label"), -} - -var testConfigNetworkAreaVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigNetworkAreaVarsMax { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") - updatedConfig["default_nameservers"] = config.StringVariable("1.1.1.2") - updatedConfig["default_prefix_length"] = config.IntegerVariable(25) - updatedConfig["max_prefix_length"] = config.IntegerVariable(25) - updatedConfig["min_prefix_length"] = config.IntegerVariable(20) - // TODO: enable once the IaaS API supports IPv6 - // updatedConfig["route_destination_type"] = config.StringVariable("cidrv6") - // updatedConfig["route_destination_value"] = config.StringVariable("2001:db8:3c4d:15::1a2b:3c4d/64") - // updatedConfig["route_next_hop_type"] = config.StringVariable("ipv6") - // updatedConfig["route_next_hop_value"] = config.StringVariable("2001:db8:3c4d:15::1a2b:3c4d") - // updatedConfig["label"] = config.StringVariable("updated") - return updatedConfig -}() - -// NETWORK AREA REGION - MIN - -var testConfigNetworkAreaRegionVarsMin = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "transfer_network": config.StringVariable("10.1.2.0/24"), - "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), -} - -var testConfigNetworkAreaRegionVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigNetworkAreaRegionVarsMin { - updatedConfig[k] = v - } - updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") - return updatedConfig -}() - -// NETWORK AREA REGION - MAX - -var testConfigNetworkAreaRegionVarsMax = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "transfer_network": config.StringVariable("10.1.2.0/24"), - "network_ranges_prefix": config.StringVariable("10.0.0.0/16"), - "default_nameservers": config.StringVariable("1.1.1.1"), - "default_prefix_length": config.IntegerVariable(26), - "min_prefix_length": config.IntegerVariable(25), - "max_prefix_length": config.IntegerVariable(28), -} - -var testConfigNetworkAreaRegionVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigNetworkAreaRegionVarsMax { - updatedConfig[k] = v - } - updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") - updatedConfig["default_nameservers"] = config.StringVariable("8.8.8.8") - updatedConfig["default_prefix_length"] = config.IntegerVariable(27) - updatedConfig["min_prefix_length"] = config.IntegerVariable(26) - updatedConfig["max_prefix_length"] = config.IntegerVariable(28) - return updatedConfig -}() - -// SECURITY GROUP - MIN - -var testConfigSecurityGroupsVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "direction": config.StringVariable("ingress"), -} - -func testConfigSecurityGroupsVarsMinUpdated() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigSecurityGroupsVarsMin { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - return updatedConfig -} - -// SECURITY GROUP - MAX - -var testConfigSecurityGroupsVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "description": config.StringVariable("description"), - "description_rule": config.StringVariable("description"), - "label": config.StringVariable("label"), - "stateful": config.BoolVariable(false), - "direction": config.StringVariable("ingress"), - "ether_type": config.StringVariable("IPv4"), - "ip_range": config.StringVariable("192.168.2.0/24"), - "port": config.StringVariable("443"), - "protocol": config.StringVariable("tcp"), - "icmp_code": config.IntegerVariable(0), - "icmp_type": config.IntegerVariable(8), - "name_remote": config.StringVariable(fmt.Sprintf("tf-acc-remote-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), -} - -func testConfigSecurityGroupsVarsMaxUpdated() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigSecurityGroupsVarsMax { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["name_remote"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name_remote"]))) - updatedConfig["description"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["description"]))) - updatedConfig["label"] = config.StringVariable("updated") - - return updatedConfig -} - -// IMAGE - MIN - -var testConfigImageVarsMin = func() config.Variables { - localFilePath := testutil.TestImageLocalFilePath - if localFilePath == "default" { - localFileForIaasImage = testutil.CreateDefaultLocalFile() - filePath, err := filepath.Abs(localFileForIaasImage.Name()) - if err != nil { - fmt.Println("Absolute path for localFileForIaasImage could not be retrieved.") - } - localFilePath = filePath - } - return config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "disk_format": config.StringVariable("qcow2"), - "local_file_path": config.StringVariable(localFilePath), - } -}() - -var testConfigImageVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigImageVarsMin { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - return updatedConfig -}() - -// IMAGE - MAX - -var testConfigImageVarsMax = func() config.Variables { - localFilePath := testutil.TestImageLocalFilePath - if localFilePath == "default" { - localFileForIaasImage = testutil.CreateDefaultLocalFile() - filePath, err := filepath.Abs(localFileForIaasImage.Name()) - if err != nil { - fmt.Println("Absolute path for localFileForIaasImage could not be retrieved.") - } - localFilePath = filePath - } - return config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "disk_format": config.StringVariable("qcow2"), - "local_file_path": config.StringVariable(localFilePath), - "min_disk_size": config.IntegerVariable(20), - "min_ram": config.IntegerVariable(2048), - "label": config.StringVariable("label"), - "boot_menu": config.BoolVariable(false), - "cdrom_bus": config.StringVariable("scsi"), - "disk_bus": config.StringVariable("scsi"), - "nic_model": config.StringVariable("e1000"), - "operating_system": config.StringVariable("linux"), - "operating_system_distro": config.StringVariable("ubuntu"), - "operating_system_version": config.StringVariable("16.04"), - "rescue_bus": config.StringVariable("sata"), - "rescue_device": config.StringVariable("cdrom"), - "secure_boot": config.BoolVariable(true), - "uefi": config.BoolVariable(true), - "video_model": config.StringVariable("vga"), - "virtio_scsi": config.BoolVariable(true), - } -}() - -var testConfigImageVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigImageVarsMax { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) - updatedConfig["min_disk_size"] = config.IntegerVariable(25) - updatedConfig["min_ram"] = config.IntegerVariable(4096) - updatedConfig["label"] = config.StringVariable("updated") - updatedConfig["boot_menu"] = config.BoolVariable(false) - updatedConfig["cdrom_bus"] = config.StringVariable("usb") - updatedConfig["disk_bus"] = config.StringVariable("usb") - updatedConfig["nic_model"] = config.StringVariable("virtio") - updatedConfig["operating_system"] = config.StringVariable("windows") - updatedConfig["operating_system_distro"] = config.StringVariable("debian") - updatedConfig["operating_system_version"] = config.StringVariable("18.04") - updatedConfig["rescue_bus"] = config.StringVariable("usb") - updatedConfig["rescue_device"] = config.StringVariable("disk") - updatedConfig["secure_boot"] = config.BoolVariable(false) - updatedConfig["uefi"] = config.BoolVariable(false) - updatedConfig["video_model"] = config.StringVariable("virtio") - updatedConfig["virtio_scsi"] = config.BoolVariable(false) - return updatedConfig -}() - -// KEYPAIR - MIN - -var testConfigKeyPairMin = config.Variables{ - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "public_key": config.StringVariable(keypairPublicKey), -} - -// KEYPAIR - MAX - -var testConfigKeyPairMax = config.Variables{ - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), - "public_key": config.StringVariable(keypairPublicKey), - "label": config.StringVariable("label"), -} - -var testConfigKeyPairMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigKeyPairMax { - updatedConfig[k] = v - } - updatedConfig["label"] = config.StringVariable("updated") - return updatedConfig -}() - -var testConfigMachineTypeVars = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), -} - -// if no local file is provided the test should create a default file and work with this instead of failing -var localFileForIaasImage os.File - -func TestAccNetworkMin(t *testing.T) { - t.Logf("TestAccNetworkMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_network.network", "region", testutil.Region), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("stackit_network.network", "region"), - resource.TestCheckNoResourceAttr("stackit_network.network", "routing_table_id"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network" "network" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id - } - `, - testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "region", testutil.Region), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "region"), - resource.TestCheckNoResourceAttr("data.stackit_network.network", "routing_table_id"), - ), - }, - - // Import - { - ConfigVariables: testConfigNetworkVarsMin, - ResourceName: "stackit_network.network", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("stackit_network.network", "region"), - resource.TestCheckNoResourceAttr("stackit_network.network", "routing_table_id"), - ), - }, - // Update - { - ConfigVariables: testConfigNetworkVarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMinUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMinUpdated["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - resource.TestCheckResourceAttrSet("stackit_network.network", "region"), - resource.TestCheckNoResourceAttr("stackit_network.network", "routing_table_id"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkMax(t *testing.T) { - t.Logf("TestAccNetworkMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network with prefix - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "stackit_network.network_prefix", "project_id", - ), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "public_ip"), - - // Network with prefix_length - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "stackit_network.network_prefix_length", "project_id", - ), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), - - resource.TestCheckResourceAttrPair( - "stackit_network.network_prefix_length", "routing_table_id", - "stackit_routing_table.routing_table", "routing_table_id", - ), - - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["organization_id"])), - resource.TestCheckResourceAttrPair( - "stackit_network_area.network_area", "network_area_id", - "stackit_routing_table.routing_table", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), - resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network" "network_prefix" { - project_id = stackit_network.network_prefix.project_id - network_id = stackit_network.network_prefix.network_id - } - - data "stackit_network" "network_prefix_length" { - project_id = stackit_network.network_prefix_length.project_id - network_id = stackit_network.network_prefix_length.network_id - } - - data "stackit_routing_table" "routing_table" { - organization_id = stackit_routing_table.routing_table.organization_id - network_area_id = stackit_routing_table.routing_table.network_area_id - routing_table_id = stackit_routing_table.routing_table.routing_table_id - } - `, - testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Network with prefix - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "data.stackit_network.network_prefix", "project_id", - ), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), - - // Network with prefix_length - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "data.stackit_network.network_prefix_length", "project_id", - ), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - // resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("data.stackit_network.network_prefix_length", "ipv4_nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckNoResourceAttr("data.stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["label"])), - resource.TestCheckResourceAttr("data.stackit_network.network_prefix_length", "region", testutil.Region), - - resource.TestCheckResourceAttrPair( - "data.stackit_network.network_prefix_length", "routing_table_id", - "data.stackit_routing_table.routing_table", "routing_table_id", - ), - - // Routing table - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["organization_id"])), - resource.TestCheckResourceAttrPair( - "stackit_network_area.network_area", "network_area_id", - "data.stackit_routing_table.routing_table", "network_area_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "0"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.Region), - resource.TestCheckNoResourceAttr("data.stackit_routing_table.routing_table", "description"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "system_routes", "true"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigNetworkVarsMax, - ResourceName: "stackit_network.network_prefix", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - projectResource, ok := s.RootModule().Resources["stackit_resourcemanager_project.project"] - if !ok { - return "", fmt.Errorf("couldn't find stackit_resourcemanager_project.project") - } - projectId, ok := projectResource.Primary.Attributes["project_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute project_id") - } - - r, ok := s.RootModule().Resources["stackit_network.network_prefix"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s,%s", projectId, testutil.Region, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "stackit_network.network_prefix", "project_id", - ), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_gateway", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_gateway"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - ), - }, - { - ConfigVariables: testConfigNetworkVarsMax, - ResourceName: "stackit_network.network_prefix_length", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - projectResource, ok := s.RootModule().Resources["stackit_resourcemanager_project.project"] - if !ok { - return "", fmt.Errorf("couldn't find stackit_resourcemanager_project.project") - } - projectId, ok := projectResource.Primary.Attributes["project_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute project_id") - } - - r, ok := s.RootModule().Resources["stackit_network.network_prefix_length"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network_prefix_length") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s,%s", projectId, testutil.Region, networkId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "stackit_network.network_prefix_length", "project_id", - ), - // resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv4_gateway"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - // nameservers may be returned in a randomized order, so we have to check them with a helper function - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_0"])), - resource.TestCheckTypeSetElemAttr("stackit_network.network_prefix_length", "nameservers.*", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_nameserver_1"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMax["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), - ), - }, - // Update - { - ConfigVariables: testConfigNetworkVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceNetworkMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "stackit_network.network_prefix", "project_id", - ), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv4_gateway"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "ipv4_prefixes.#", "1"), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["label"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix", "public_ip"), - - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "network_id"), - resource.TestCheckResourceAttrPair( - "stackit_resourcemanager_project.project", "project_id", - "stackit_network.network_prefix_length", "project_id", - ), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_gateway"), - // resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "no_ipv4_gateway", "true"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.#", "2"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_0"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_nameservers.1", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_nameserver_1"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "ipv4_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["ipv4_prefix_length"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "ipv4_prefix"), - resource.TestCheckNoResourceAttr("stackit_network.network_prefix_length", "ipv6_prefixes.#"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "routed", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["routed"])), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["label"])), - resource.TestCheckResourceAttrSet("stackit_network.network_prefix_length", "public_ip"), - resource.TestCheckResourceAttr("stackit_network.network_prefix_length", "region", testutil.Region), - - resource.TestCheckResourceAttrPair( - "stackit_network.network_prefix_length", "routing_table_id", - "stackit_routing_table.routing_table", "routing_table_id", - ), - - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["organization_id"])), - resource.TestCheckResourceAttrPair( - "stackit_network_area.network_area", "network_area_id", - "stackit_routing_table.routing_table", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigNetworkVarsMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), - resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkAreaMin(t *testing.T) { - t.Logf("TestAccNetworkAreaMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkAreaVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["organization_id"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "0"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkAreaVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network_area" "network_area" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkAreaMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["organization_id"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area.network_area", "network_area_id"), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area.network_area", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMin["name"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.#", "0"), - ), - }, - // Import - { - ConfigVariables: testConfigNetworkAreaVarsMinUpdated, - ResourceName: "stackit_network_area.network_area", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area.network_area"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area.network_area") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigNetworkAreaVarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["organization_id"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMinUpdated["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "0"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkAreaMax(t *testing.T) { - t.Logf("TestAccNetworkAreaMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkAreaVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["organization_id"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["label"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_nameservers"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["max_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["min_prefix_length"])), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "organization_id", - "stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_type"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_value"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_type"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_value"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["label"])), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkAreaVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network_area" "network_area" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - } - - data "stackit_network_area_route" "network_area_route" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - network_area_route_id = stackit_network_area_route.network_area_route.network_area_route_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkAreaMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["organization_id"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area.network_area", "network_area_id"), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area.network_area", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area.network_area", "network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["label"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "default_nameservers.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_nameservers"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["max_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network_area.network_area", "min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["min_prefix_length"])), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "organization_id", - "data.stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "network_area_id", - "data.stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_route.network_area_route", "network_area_route_id", - "stackit_network_area_route.network_area_route", "network_area_route_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "destination.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_type"])), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "destination.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_destination_value"])), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "next_hop.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_type"])), - resource.TestCheckResourceAttr("data.stackit_network_area_route.network_area_route", "next_hop.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["route_next_hop_value"])), - ), - }, - // Import - { - ConfigVariables: testConfigNetworkAreaVarsMaxUpdated, - ResourceName: "stackit_network_area.network_area", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area.network_area"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area.network_area") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["organization_id"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["label"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_nameservers"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["default_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["max_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMax["min_prefix_length"])), - ), - }, - { - ConfigVariables: testConfigNetworkAreaVarsMaxUpdated, - ResourceName: "stackit_network_area_route.network_area_route", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area_route.network_area_route"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area_route.network_area_route") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - networkAreaRouteId, ok := r.Primary.Attributes["network_area_route_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_route_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region, networkAreaRouteId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigNetworkAreaVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["organization_id"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_area_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "name", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area.network_area", "network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["label"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["default_nameservers"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["default_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["max_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area.network_area", "min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["min_prefix_length"])), - - // Network Area Route - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "organization_id", - "stackit_network_area.network_area", "organization_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_area_route.network_area_route", "network_area_id", - "stackit_network_area.network_area", "network_area_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_area_route.network_area_route", "network_area_route_id"), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_destination_type"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "destination.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_destination_value"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.type", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_next_hop_type"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "next_hop.value", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["route_next_hop_value"])), - resource.TestCheckResourceAttr("stackit_network_area_route.network_area_route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkAreaVarsMaxUpdated["label"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkAreaRegionMin(t *testing.T) { - t.Logf("TestAccNetworkAreaRegionMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkAreaRegionVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionCreate), - plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["organization_id"])), - resource.TestCheckResourceAttrPair( - "stackit_network_area.network_area", "network_area_id", - "stackit_network_area_region.network_area_region", "network_area_id", - ), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["transfer_network"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), - resource.TestCheckNoResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkAreaRegionVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network_area_region" "network_area_region" { - organization_id = stackit_network_area_region.network_area_region.organization_id - network_area_id = stackit_network_area_region.network_area_region.network_area_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["organization_id"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "network_area_id"), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_region.network_area_region", "network_area_id", - "stackit_network_area_region.network_area_region", "network_area_id", - ), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["transfer_network"])), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMin["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value - ), - }, - // Import - { - ConfigVariables: testConfigNetworkAreaRegionVarsMinUpdated, - ResourceName: "stackit_network_area_region.network_area_region", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area_region.network_area_region"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area_region.network_area_region") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigNetworkAreaRegionVarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["organization_id"])), - resource.TestCheckResourceAttrPair( - "stackit_network_area.network_area", "network_area_id", - "stackit_network_area_region.network_area_region", "network_area_id", - ), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["transfer_network"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMinUpdated["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", "25"), // default value - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", "24"), // default value - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", "29"), // default value - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkAreaRegionMax(t *testing.T) { - t.Logf("TestAccNetworkAreaRegionMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkAreaRegionVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionCreate), - plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["organization_id"])), - resource.TestCheckResourceAttrPair( - "stackit_network_area.network_area", "network_area_id", - "stackit_network_area_region.network_area_region", "network_area_id", - ), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["transfer_network"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_nameservers"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["min_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["max_prefix_length"])), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkAreaRegionVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network_area_region" "network_area_region" { - organization_id = stackit_network_area_region.network_area_region.organization_id - network_area_id = stackit_network_area_region.network_area_region.network_area_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["organization_id"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "network_area_id"), - resource.TestCheckResourceAttrPair( - "data.stackit_network_area_region.network_area_region", "network_area_id", - "stackit_network_area_region.network_area_region", "network_area_id", - ), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["transfer_network"])), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("data.stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_nameservers"])), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["default_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["min_prefix_length"])), - resource.TestCheckResourceAttr("data.stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMax["max_prefix_length"])), - ), - }, - // Import - { - ConfigVariables: testConfigNetworkAreaRegionVarsMaxUpdated, - ResourceName: "stackit_network_area_region.network_area_region", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_area_region.network_area_region"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_area_region.network_area_region") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, testutil.Region), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigNetworkAreaRegionVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkAreaRegionMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_network_area.network_area", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_network_area_region.network_area_region", plancheck.ResourceActionUpdate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - // Network Area - resource.TestCheckResourceAttr("stackit_network_area.network_area", "organization_id", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["organization_id"])), - resource.TestCheckResourceAttrPair( - "stackit_network_area.network_area", "network_area_id", - "stackit_network_area_region.network_area_region", "network_area_id", - ), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.transfer_network", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["transfer_network"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.prefix", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["network_ranges_prefix"])), - resource.TestCheckResourceAttrSet("stackit_network_area_region.network_area_region", "ipv4.network_ranges.0.network_range_id"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.#", "1"), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_nameservers.0", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["default_nameservers"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.default_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["default_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.min_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["min_prefix_length"])), - resource.TestCheckResourceAttr("stackit_network_area_region.network_area_region", "ipv4.max_prefix_length", testutil.ConvertConfigVariable(testConfigNetworkAreaRegionVarsMaxUpdated["max_prefix_length"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccVolumeMin(t *testing.T) { - t.Logf("TestAccVolumeMin name: null") - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVolumeVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceVolumeMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Volume size - resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "performance_class"), - resource.TestCheckNoResourceAttr("stackit_volume.volume_size", "server_id"), - - // Volume source - resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "performance_class"), - resource.TestCheckResourceAttrPair( - "stackit_volume.volume_source", "source.id", - "stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "source.type", "volume"), - resource.TestCheckNoResourceAttr("stackit_volume.volume_source", "server_id"), - ), - }, - // Data source - { - ConfigVariables: testConfigVolumeVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_volume" "volume_size" { - project_id = stackit_volume.volume_size.project_id - volume_id = stackit_volume.volume_size.volume_id - } - - data "stackit_volume" "volume_source" { - project_id = stackit_volume.volume_source.project_id - volume_id = stackit_volume.volume_source.volume_id - } - `, - testutil.IaaSProviderConfig(), resourceVolumeMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Volume size - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_volume.volume_size", "volume_id", - "data.stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "region", testutil.Region), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), - resource.TestCheckResourceAttrSet("data.stackit_volume.volume_size", "performance_class"), - resource.TestCheckNoResourceAttr("data.stackit_volume.volume_size", "server_id"), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), - - // Volume source - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_volume.volume_source", "volume_id", - "data.stackit_volume.volume_source", "volume_id", - ), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["availability_zone"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), - resource.TestCheckResourceAttrSet("data.stackit_volume.volume_source", "performance_class"), - resource.TestCheckNoResourceAttr("data.stackit_volume.volume_source", "server_id"), - resource.TestCheckResourceAttrPair( - "data.stackit_volume.volume_source", "source.id", - "data.stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "source.type", "volume"), - ), - }, - // Import - { - ConfigVariables: testConfigVolumeVarsMin, - ResourceName: "stackit_volume.volume_size", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_volume.volume_size"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_volume.volume_size") - } - volumeId, ok := r.Primary.Attributes["volume_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigVolumeVarsMin, - ResourceName: "stackit_volume.volume_source", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_volume.volume_source"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_volume.volume") - } - volumeId, ok := r.Primary.Attributes["volume_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigVolumeVarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceVolumeMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Volume size - resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["size"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "performance_class"), - resource.TestCheckNoResourceAttr("stackit_volume.volume_size", "server_id"), - - // Volume source - resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMinUpdated["availability_zone"])), - // Volume from source doesn't change size. So here the initial size will be used - resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMin["size"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "performance_class"), - resource.TestCheckResourceAttrPair( - "stackit_volume.volume_source", "source.id", - "stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "source.type", "volume"), - resource.TestCheckNoResourceAttr("stackit_volume.volume_source", "server_id"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccVolumeMax(t *testing.T) { - t.Logf("TestAccVolumeMax name: %s", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVolumeVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceVolumeMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Volume size - resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "performance_class", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["performance_class"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "name", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "labels.acc-test", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["label"])), - resource.TestCheckNoResourceAttr("stackit_volume.volume_size", "server_id"), - - // Volume source - resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "performance_class", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["performance_class"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "name", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "labels.acc-test", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["label"])), - resource.TestCheckResourceAttrPair( - "stackit_volume.volume_source", "source.id", - "stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "source.type", "volume"), - resource.TestCheckNoResourceAttr("stackit_volume.volume_source", "server_id"), - ), - }, - // Data source - { - ConfigVariables: testConfigVolumeVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_volume" "volume_size" { - project_id = stackit_volume.volume_size.project_id - volume_id = stackit_volume.volume_size.volume_id - } - - data "stackit_volume" "volume_source" { - project_id = stackit_volume.volume_source.project_id - volume_id = stackit_volume.volume_source.volume_id - } - `, - testutil.IaaSProviderConfig(), resourceVolumeMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Volume size - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_volume.volume_size", "volume_id", - "data.stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "region", testutil.Region), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), - resource.TestCheckNoResourceAttr("data.stackit_volume.volume_size", "server_id"), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "performance_class", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["performance_class"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "name", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_volume.volume_size", "labels.acc-test", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["label"])), - - // Volume source - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_volume.volume_source", "volume_id"), - resource.TestCheckResourceAttrPair( - "data.stackit_volume.volume_source", "volume_id", - "stackit_volume.volume_source", "volume_id", - ), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["availability_zone"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["size"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "performance_class", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["performance_class"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "name", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "labels.acc-test", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["label"])), - resource.TestCheckResourceAttrPair( - "data.stackit_volume.volume_source", "source.id", - "data.stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("data.stackit_volume.volume_source", "source.type", "volume"), - resource.TestCheckNoResourceAttr("data.stackit_volume.volume_source", "server_id"), - ), - }, - // Import - { - ConfigVariables: testConfigVolumeVarsMax, - ResourceName: "stackit_volume.volume_size", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_volume.volume_size"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_volume.volume_size") - } - volumeId, ok := r.Primary.Attributes["volume_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigVolumeVarsMax, - ResourceName: "stackit_volume.volume_source", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_volume.volume_source"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_volume.volume_source") - } - volumeId, ok := r.Primary.Attributes["volume_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigVolumeVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceVolumeMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Volume size - resource.TestCheckResourceAttr("stackit_volume.volume_size", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_size", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["size"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["description"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "performance_class", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["performance_class"])), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "name", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["name"])), - resource.TestCheckNoResourceAttr("stackit_volume.volume_size", "server_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_volume.volume_size", "labels.acc-test", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["label"])), - - // Volume source - resource.TestCheckResourceAttr("stackit_volume.volume_source", "project_id", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_volume.volume_source", "volume_id"), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "availability_zone", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "size", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["size"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "description", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["description"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "performance_class", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["performance_class"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "name", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "labels.acc-test", testutil.ConvertConfigVariable(testConfigVolumeVarsMaxUpdated["label"])), - resource.TestCheckResourceAttrPair( - "stackit_volume.volume_source", "source.id", - "stackit_volume.volume_size", "volume_id", - ), - resource.TestCheckResourceAttr("stackit_volume.volume_source", "source.type", "volume"), - resource.TestCheckNoResourceAttr("stackit_volume.volume_source", "server_id"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccServerMin(t *testing.T) { - t.Logf("TestAccServerMin name: %s", testutil.ConvertConfigVariable(testConfigServerVarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigServerVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceServerMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Server - resource.TestCheckResourceAttr("stackit_server.server", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_server.server", "name", testutil.ConvertConfigVariable(testConfigServerVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_server.server", "machine_type", testutil.ConvertConfigVariable(testConfigServerVarsMin["machine_type"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.%"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", "image"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_id", testutil.ConvertConfigVariable(testConfigServerVarsMin["image_id"])), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.delete_on_termination", "true"), - resource.TestCheckNoResourceAttr("stackit_server.server", "boot_volume.performance_class"), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.size"), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.id"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", "image"), - resource.TestCheckNoResourceAttr("stackit_server.server", "image_id"), - resource.TestCheckResourceAttr("stackit_server.server", "labels.%", "0"), - resource.TestCheckResourceAttrSet("stackit_server.server", "server_id"), - resource.TestCheckResourceAttrSet("stackit_server.server", "availability_zone"), - resource.TestCheckNoResourceAttr("stackit_server.server", "desired_status"), - resource.TestCheckNoResourceAttr("stackit_server.server", "user_data"), - resource.TestCheckNoResourceAttr("stackit_server.server", "keypair_name"), - resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "network_interfaces.0", - "stackit_network_interface.nic", "network_interface_id", - ), - resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"), - resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"), - resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"), - ), - }, - // Data source - { - ConfigVariables: testConfigServerVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_server" "server" { - project_id = stackit_server.server.project_id - server_id = stackit_server.server.server_id - } - `, - testutil.IaaSProviderConfig(), resourceServerMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Server - resource.TestCheckResourceAttr("data.stackit_server.server", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_server.server", "name", testutil.ConvertConfigVariable(testConfigServerVarsMin["name"])), - resource.TestCheckResourceAttr("data.stackit_server.server", "machine_type", testutil.ConvertConfigVariable(testConfigServerVarsMin["machine_type"])), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "boot_volume.%"), - // boot_volume.attributes are unknown in the datasource. only boot_volume.id and boot_volume.delete_on_termination are returned from the api - resource.TestCheckNoResourceAttr("data.stackit_server.server", "boot_volume.source_type"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "boot_volume.source_id"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "boot_volume.size"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "boot_volume.performance_class"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "boot_volume.source_type"), - resource.TestCheckResourceAttr("data.stackit_server.server", "boot_volume.delete_on_termination", "true"), - resource.TestCheckResourceAttrPair( - "data.stackit_server.server", "boot_volume.id", - "stackit_server.server", "boot_volume.id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_server.server", "server_id", - "stackit_server.server", "server_id", - ), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "image_id"), - resource.TestCheckResourceAttr("data.stackit_server.server", "labels.%", "0"), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "server_id"), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "availability_zone"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "desired_status"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "user_data"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "keypair_name"), - resource.TestCheckResourceAttr("data.stackit_server.server", "network_interfaces.#", "1"), - resource.TestCheckResourceAttrPair( - "data.stackit_server.server", "network_interfaces.0", - "stackit_network_interface.nic", "network_interface_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "launched_at"), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigServerVarsMin, - ResourceName: "stackit_server.server", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server.server"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server.server") - } - serverId, ok := r.Primary.Attributes["server_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute server_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, serverId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"boot_volume", "network_interfaces"}, // Field is not mapped as it is only relevant on creation - }, - // Update - { - ConfigVariables: testConfigServerVarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceServerMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Server - resource.TestCheckResourceAttr("stackit_server.server", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMinUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_server.server", "name", testutil.ConvertConfigVariable(testConfigServerVarsMinUpdated["name"])), - resource.TestCheckResourceAttr("stackit_server.server", "machine_type", testutil.ConvertConfigVariable(testConfigServerVarsMinUpdated["machine_type"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.%"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", "image"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_id", testutil.ConvertConfigVariable(testConfigServerVarsMinUpdated["image_id"])), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.delete_on_termination", "true"), - resource.TestCheckNoResourceAttr("stackit_server.server", "boot_volume.performance_class"), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.size"), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.id"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", "image"), - resource.TestCheckNoResourceAttr("stackit_server.server", "image_id"), - resource.TestCheckResourceAttr("stackit_server.server", "labels.%", "0"), - resource.TestCheckResourceAttrSet("stackit_server.server", "server_id"), - resource.TestCheckResourceAttrSet("stackit_server.server", "availability_zone"), - resource.TestCheckNoResourceAttr("stackit_server.server", "desired_status"), - resource.TestCheckNoResourceAttr("stackit_server.server", "user_data"), - resource.TestCheckNoResourceAttr("stackit_server.server", "keypair_name"), - resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "network_interfaces.0", - "stackit_network_interface.nic", "network_interface_id", - ), - resource.TestCheckResourceAttrSet("stackit_server.server", "created_at"), - resource.TestCheckResourceAttrSet("stackit_server.server", "launched_at"), - resource.TestCheckResourceAttrSet("stackit_server.server", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccServerMax(t *testing.T) { - t.Logf("TestAccServerMax name: %s", testutil.ConvertConfigVariable(testConfigServerVarsMax["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigServerVarsMax, - Config: fmt.Sprintf("%s\n%s\n%s", testutil.IaaSProviderConfig(), resourceServerMaxConfig, resourceServerMaxAttachmentConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Affinity group - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "name", testutil.ConvertConfigVariable(testConfigServerVarsMax["name_not_updated"])), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "policy", testutil.ConvertConfigVariable(testConfigServerVarsMax["policy"])), - resource.TestCheckResourceAttrSet("stackit_affinity_group.affinity_group", "affinity_group_id"), - - // Volume base - resource.TestCheckResourceAttr("stackit_volume.base_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMax["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "size", testutil.ConvertConfigVariable(testConfigServerVarsMax["size"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "source.id", testutil.ConvertConfigVariable(testConfigServerVarsMax["image_id"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "source.type", "image"), - resource.TestCheckResourceAttrSet("stackit_volume.base_volume", "volume_id"), - resource.TestCheckResourceAttrPair( - "stackit_volume.base_volume", "volume_id", - "stackit_server.server", "boot_volume.source_id", - ), - - // Volume data - resource.TestCheckResourceAttr("stackit_volume.data_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_volume.data_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMax["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.data_volume", "size", testutil.ConvertConfigVariable(testConfigServerVarsMax["size"])), - resource.TestCheckResourceAttrSet("stackit_volume.data_volume", "volume_id"), - - // Volume data attach - resource.TestCheckResourceAttr("stackit_server_volume_attach.data_volume_attachment", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server_volume_attach.data_volume_attachment", "server_id"), - resource.TestCheckResourceAttrPair( - "stackit_server_volume_attach.data_volume_attachment", "server_id", - "stackit_server.server", "server_id", - ), - resource.TestCheckResourceAttrSet("stackit_server_volume_attach.data_volume_attachment", "volume_id"), - resource.TestCheckResourceAttrPair( - "stackit_volume.data_volume", "volume_id", - "stackit_server_volume_attach.data_volume_attachment", "volume_id", - ), - - // Network - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigServerVarsMax["name"])), - - // Network interface init - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_init", "project_id", - "stackit_network.network", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_init", "network_id", - "stackit_network.network", "network_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_init", "network_interface_id"), - - // Network interface second - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_second", "project_id", - "stackit_network.network", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_second", "network_id", - "stackit_network.network", "network_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_second", "network_interface_id"), - - // Network interface attachment - resource.TestCheckResourceAttr("stackit_server_network_interface_attach.network_interface_second_attachment", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_server_network_interface_attach.network_interface_second_attachment", "network_interface_id", - "stackit_network_interface.network_interface_second", "network_interface_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_server_network_interface_attach.network_interface_second_attachment", "server_id", - "stackit_server.server", "server_id", - ), - - // Keypair - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigServerVarsMax["name_not_updated"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigServerVarsMax["public_key"])), - - // Service account attachment - resource.TestCheckResourceAttrPair( - "stackit_server_service_account_attach.attached_service_account", "project_id", - "stackit_server.server", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_server_service_account_attach.attached_service_account", "server_id", - "stackit_server.server", "server_id", - ), - resource.TestCheckResourceAttr( - "stackit_server_service_account_attach.attached_service_account", "service_account_email", - testutil.ConvertConfigVariable(testConfigServerVarsMax["service_account_mail"]), - ), - - // Server - resource.TestCheckResourceAttr("stackit_server.server", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "server_id"), - resource.TestCheckResourceAttr("stackit_server.server", "name", testutil.ConvertConfigVariable(testConfigServerVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_server.server", "machine_type", testutil.ConvertConfigVariable(testConfigServerVarsMax["machine_type"])), - resource.TestCheckResourceAttr("stackit_server.server", "desired_status", testutil.ConvertConfigVariable(testConfigServerVarsMax["desired_status"])), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "affinity_group", - "stackit_affinity_group.affinity_group", "affinity_group_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMax["availability_zone"])), - resource.TestCheckResourceAttrPair( - "stackit_key_pair.key_pair", "name", - "stackit_server.server", "keypair_name", - ), - // The network interface which was attached by "stackit_server_network_interface_attach" should not appear here - resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "network_interfaces.0", - "stackit_network_interface.network_interface_init", "network_interface_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "user_data", testutil.ConvertConfigVariable(testConfigServerVarsMax["user_data"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.id"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", "volume"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "boot_volume.source_id", - "stackit_volume.base_volume", "volume_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "labels.acc-test", testutil.ConvertConfigVariable(testConfigServerVarsMax["label"])), - ), - }, - // Data source - { - ConfigVariables: testConfigServerVarsMax, - Config: fmt.Sprintf(` - %s - %s - %s - - data "stackit_server" "server" { - project_id = stackit_server.server.project_id - server_id = stackit_server.server.server_id - } - `, - testutil.IaaSProviderConfig(), resourceServerMaxConfig, resourceServerMaxAttachmentConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Server - resource.TestCheckResourceAttr("data.stackit_server.server", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "server_id"), - resource.TestCheckResourceAttr("data.stackit_server.server", "name", testutil.ConvertConfigVariable(testConfigServerVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_server.server", "machine_type", testutil.ConvertConfigVariable(testConfigServerVarsMax["machine_type"])), - resource.TestCheckResourceAttrPair( - "data.stackit_server.server", "affinity_group", - "stackit_affinity_group.affinity_group", "affinity_group_id", - ), - resource.TestCheckResourceAttr("data.stackit_server.server", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMax["availability_zone"])), - resource.TestCheckResourceAttrPair( - "stackit_key_pair.key_pair", "name", - "data.stackit_server.server", "keypair_name", - ), - // All network interface which was are attached appear here - resource.TestCheckResourceAttr("data.stackit_server.server", "network_interfaces.#", "2"), - resource.TestCheckTypeSetElemAttrPair( - "data.stackit_server.server", "network_interfaces.*", - "stackit_network_interface.network_interface_init", "network_interface_id", - ), - resource.TestCheckTypeSetElemAttrPair( - "data.stackit_server.server", "network_interfaces.*", - "stackit_network_interface.network_interface_second", "network_interface_id", - ), - resource.TestCheckResourceAttr("data.stackit_server.server", "user_data", testutil.ConvertConfigVariable(testConfigServerVarsMax["user_data"])), - resource.TestCheckResourceAttrSet("data.stackit_server.server", "boot_volume.id"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "boot_volume.source_type"), - resource.TestCheckNoResourceAttr("data.stackit_server.server", "boot_volume.source_id"), - resource.TestCheckResourceAttr("data.stackit_server.server", "labels.acc-test", testutil.ConvertConfigVariable(testConfigServerVarsMax["label"])), - ), - }, - // Import - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_affinity_group.affinity_group", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_affinity_group.affinity_group"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_affinity_group.affinity_group") - } - affinityGroupId, ok := r.Primary.Attributes["affinity_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute affinity_group_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, affinityGroupId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_volume.base_volume", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_volume.base_volume"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_volume.base_volume") - } - volumeId, ok := r.Primary.Attributes["volume_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_volume.data_volume", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_volume.data_volume"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_volume.data_volume") - } - volumeId, ok := r.Primary.Attributes["volume_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, volumeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_server_volume_attach.data_volume_attachment", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server_volume_attach.data_volume_attachment"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server_volume_attach.data_volume_attachment") - } - serverId, ok := r.Primary.Attributes["server_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute server_id") - } - volumeId, ok := r.Primary.Attributes["volume_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, volumeId), nil - }, - ImportState: true, - ImportStateVerify: false, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_network.network", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"ipv4_prefix_length", "ipv4_prefix"}, // Field is not returned by the API - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_network_interface.network_interface_init", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_interface.network_interface_init"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_interface.network_interface_init") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - networkInterfaceId, ok := r.Primary.Attributes["network_interface_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_interface_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_network_interface.network_interface_second", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_interface.network_interface_second"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_interface.network_interface_second") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - networkInterfaceId, ok := r.Primary.Attributes["network_interface_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_interface_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_server_network_interface_attach.network_interface_second_attachment", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server_network_interface_attach.network_interface_second_attachment"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server_network_interface_attach.network_interface_second_attachment") - } - serverId, ok := r.Primary.Attributes["server_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - networkInterfaceId, ok := r.Primary.Attributes["network_interface_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_interface_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, networkInterfaceId), nil - }, - ImportState: true, - ImportStateVerify: false, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_key_pair.key_pair", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_key_pair.key_pair"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_key_pair.key_pair") - } - keyPairName, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return keyPairName, nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_server_service_account_attach.attached_service_account", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server_service_account_attach.attached_service_account"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server_service_account_attach.attached_service_account") - } - serverId, ok := r.Primary.Attributes["server_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute server_id") - } - serviceAccountEmail, ok := r.Primary.Attributes["service_account_email"] - if !ok { - return "", fmt.Errorf("couldn't find attribute volume_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, serverId, serviceAccountEmail), nil - }, - ImportState: true, - ImportStateVerify: false, - }, - { - ConfigVariables: testConfigServerVarsMax, - ResourceName: "stackit_server.server", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server.server"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server.server") - } - serverId, ok := r.Primary.Attributes["server_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute server_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, serverId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"boot_volume", "desired_status", "network_interfaces"}, // Field is not mapped as it is only relevant on creation - }, - // Update - { - ConfigVariables: testConfigServerVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceServerMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Affinity group - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["name_not_updated"])), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "policy", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["policy"])), - resource.TestCheckResourceAttrSet("stackit_affinity_group.affinity_group", "affinity_group_id"), - - // Volume base - resource.TestCheckResourceAttr("stackit_volume.base_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "size", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["size"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "source.id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["image_id"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "source.type", "image"), - resource.TestCheckResourceAttrSet("stackit_volume.base_volume", "volume_id"), - resource.TestCheckResourceAttrPair( - "stackit_volume.base_volume", "volume_id", - "stackit_server.server", "boot_volume.source_id", - ), - - // Volume data - resource.TestCheckResourceAttr("stackit_volume.data_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_volume.data_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.data_volume", "size", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["size"])), - resource.TestCheckResourceAttrSet("stackit_volume.data_volume", "volume_id"), - - // Network - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["name"])), - - // Network interface init - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_init", "project_id", - "stackit_network.network", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_init", "network_id", - "stackit_network.network", "network_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_init", "network_interface_id"), - - // Network interface second - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_second", "project_id", - "stackit_network.network", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_second", "network_id", - "stackit_network.network", "network_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_second", "network_interface_id"), - - // Keypair - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["name_not_updated"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["public_key"])), - - // Service account attachment - resource.TestCheckResourceAttrPair( - "stackit_server_service_account_attach.attached_service_account", "project_id", - "stackit_server.server", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_server_service_account_attach.attached_service_account", "server_id", - "stackit_server.server", "server_id", - ), - resource.TestCheckResourceAttr( - "stackit_server_service_account_attach.attached_service_account", "service_account_email", - testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["service_account_mail"]), - ), - - // Server - resource.TestCheckResourceAttr("stackit_server.server", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "server_id"), - resource.TestCheckResourceAttr("stackit_server.server", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_server.server", "machine_type", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["machine_type"])), - resource.TestCheckResourceAttr("stackit_server.server", "desired_status", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["desired_status"])), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "affinity_group", - "stackit_affinity_group.affinity_group", "affinity_group_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["availability_zone"])), - resource.TestCheckResourceAttrPair( - "stackit_key_pair.key_pair", "name", - "stackit_server.server", "keypair_name", - ), - // The network interface which was attached by "stackit_server_network_interface_attach" should not appear here - resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "network_interfaces.0", - "stackit_network_interface.network_interface_init", "network_interface_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "user_data", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["user_data"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.id"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", "volume"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "boot_volume.source_id", - "stackit_volume.base_volume", "volume_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "labels.acc-test", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdated["label"])), - ), - }, - // Updated desired status - { - ConfigVariables: testConfigServerVarsMaxUpdatedDesiredStatus, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceServerMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Affinity group - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["project_id"])), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["name_not_updated"])), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "policy", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["policy"])), - resource.TestCheckResourceAttrSet("stackit_affinity_group.affinity_group", "affinity_group_id"), - - // Volume base - resource.TestCheckResourceAttr("stackit_volume.base_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["project_id"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "size", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["size"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "source.id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["image_id"])), - resource.TestCheckResourceAttr("stackit_volume.base_volume", "source.type", "image"), - resource.TestCheckResourceAttrSet("stackit_volume.base_volume", "volume_id"), - resource.TestCheckResourceAttrPair( - "stackit_volume.base_volume", "volume_id", - "stackit_server.server", "boot_volume.source_id", - ), - - // Volume data - resource.TestCheckResourceAttr("stackit_volume.data_volume", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["project_id"])), - resource.TestCheckResourceAttr("stackit_volume.data_volume", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["availability_zone"])), - resource.TestCheckResourceAttr("stackit_volume.data_volume", "size", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["size"])), - resource.TestCheckResourceAttrSet("stackit_volume.data_volume", "volume_id"), - - // Network - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["project_id"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["name"])), - - // Network interface init - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_init", "project_id", - "stackit_network.network", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_init", "network_id", - "stackit_network.network", "network_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_init", "network_interface_id"), - - // Network interface second - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_second", "project_id", - "stackit_network.network", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_second", "network_id", - "stackit_network.network", "network_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_second", "network_interface_id"), - - // Keypair - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["name_not_updated"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["public_key"])), - - // Service account attachment - resource.TestCheckResourceAttrPair( - "stackit_server_service_account_attach.attached_service_account", "project_id", - "stackit_server.server", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_server_service_account_attach.attached_service_account", "server_id", - "stackit_server.server", "server_id", - ), - resource.TestCheckResourceAttr( - "stackit_server_service_account_attach.attached_service_account", "service_account_email", - testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["service_account_mail"]), - ), - - // Server - resource.TestCheckResourceAttr("stackit_server.server", "project_id", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "server_id"), - resource.TestCheckResourceAttr("stackit_server.server", "name", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["name"])), - resource.TestCheckResourceAttr("stackit_server.server", "machine_type", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["machine_type"])), - resource.TestCheckResourceAttr("stackit_server.server", "desired_status", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["desired_status"])), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "affinity_group", - "stackit_affinity_group.affinity_group", "affinity_group_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "availability_zone", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["availability_zone"])), - resource.TestCheckResourceAttrPair( - "stackit_key_pair.key_pair", "name", - "stackit_server.server", "keypair_name", - ), - // The network interface which was attached by "stackit_server_network_interface_attach" should not appear here - resource.TestCheckResourceAttr("stackit_server.server", "network_interfaces.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "network_interfaces.0", - "stackit_network_interface.network_interface_init", "network_interface_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "user_data", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["user_data"])), - resource.TestCheckResourceAttrSet("stackit_server.server", "boot_volume.id"), - resource.TestCheckResourceAttr("stackit_server.server", "boot_volume.source_type", "volume"), - resource.TestCheckResourceAttrPair( - "stackit_server.server", "boot_volume.source_id", - "stackit_volume.base_volume", "volume_id", - ), - resource.TestCheckResourceAttr("stackit_server.server", "labels.acc-test", testutil.ConvertConfigVariable(testConfigServerVarsMaxUpdatedDesiredStatus["label"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccAffinityGroupMin(t *testing.T) { - t.Logf("TestAccAffinityGroupMin name: %s", testutil.ConvertConfigVariable(testConfigAffinityGroupVarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigAffinityGroupVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceAffinityGroupMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "project_id", testutil.ConvertConfigVariable(testConfigAffinityGroupVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_affinity_group.affinity_group", "affinity_group_id"), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "name", testutil.ConvertConfigVariable(testConfigAffinityGroupVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_affinity_group.affinity_group", "policy", testutil.ConvertConfigVariable(testConfigAffinityGroupVarsMin["policy"])), - resource.TestCheckNoResourceAttr("stackit_affinity_group.affinity_group", "members.#"), - ), - }, - // Data source - { - ConfigVariables: testConfigAffinityGroupVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_affinity_group" "affinity_group" { - project_id = stackit_affinity_group.affinity_group.project_id - affinity_group_id = stackit_affinity_group.affinity_group.affinity_group_id - } - `, - testutil.IaaSProviderConfig(), resourceAffinityGroupMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_affinity_group.affinity_group", "project_id", testutil.ConvertConfigVariable(testConfigAffinityGroupVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_affinity_group.affinity_group", "affinity_group_id", - "data.stackit_affinity_group.affinity_group", "affinity_group_id", - ), - resource.TestCheckResourceAttr("data.stackit_affinity_group.affinity_group", "name", testutil.ConvertConfigVariable(testConfigAffinityGroupVarsMin["name"])), - ), - }, - // Import - { - ConfigVariables: testConfigAffinityGroupVarsMin, - ResourceName: "stackit_affinity_group.affinity_group", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_affinity_group.affinity_group"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_affinity_group.affinity_group") - } - affinityGroupId, ok := r.Primary.Attributes["affinity_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute affinity_group_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, affinityGroupId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // In this minimal setup, no update can be performed - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccIaaSSecurityGroupMin(t *testing.T) { - t.Logf("TestAccIaaSSecurityGroupMin name: %s", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - - // Creation - { - ConfigVariables: testConfigSecurityGroupsVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceSecurityGroupMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Security Group - resource.TestCheckResourceAttr("stackit_security_group.security_group", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group", "security_group_id"), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group", "stateful"), - - // Security Group Rule - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMin["direction"])), - ), - }, - // Data source - { - ConfigVariables: testConfigSecurityGroupsVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_security_group" "security_group" { - project_id = stackit_security_group.security_group.project_id - security_group_id = stackit_security_group.security_group.security_group_id - } - - data "stackit_security_group_rule" "security_group_rule" { - project_id = stackit_security_group.security_group.project_id - security_group_id = stackit_security_group.security_group.security_group_id - security_group_rule_id = stackit_security_group_rule.security_group_rule.security_group_rule_id - } - `, - testutil.IaaSProviderConfig(), resourceSecurityGroupMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_security_group.security_group", "security_group_id", - "data.stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMin["name"])), - - // Security Group Rule - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMin["direction"])), - ), - }, - // Import - { - ConfigVariables: testConfigSecurityGroupsVarsMin, - ResourceName: "stackit_security_group.security_group", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_security_group.security_group"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_security_group.security_group") - } - securityGroupId, ok := r.Primary.Attributes["security_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute security_group_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigSecurityGroupsVarsMin, - ResourceName: "stackit_security_group_rule.security_group_rule", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_security_group_rule.security_group_rule"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_security_group_rule.security_group_rule") - } - securityGroupId, ok := r.Primary.Attributes["security_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute security_group_id") - } - securityGroupRuleId, ok := r.Primary.Attributes["security_group_rule_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute security_group_rule_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId, securityGroupRuleId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigSecurityGroupsVarsMinUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceSecurityGroupMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Security Group - resource.TestCheckResourceAttr("stackit_security_group.security_group", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group", "security_group_id"), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMinUpdated()["name"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group", "stateful"), - - // Security Group Rule - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMinUpdated()["direction"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccIaaSSecurityGroupMax(t *testing.T) { - t.Logf("TestAccIaaSSecurityGroupMax name: %s", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - - // Creation - { - ConfigVariables: testConfigSecurityGroupsVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceSecurityGroupMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Security Group (default) - resource.TestCheckResourceAttr("stackit_security_group.security_group", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group", "security_group_id"), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["description"])), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "stateful", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["stateful"])), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "labels.acc-test", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["label"])), - - // Security Group (remote) - resource.TestCheckResourceAttr("stackit_security_group.security_group_remote", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group_remote", "security_group_id"), - resource.TestCheckResourceAttr("stackit_security_group.security_group_remote", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["name_remote"])), - - // Security Group Rule (default) - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["direction"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["description_rule"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "ether_type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ether_type"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "port_range.min", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["port"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "port_range.max", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["port"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "protocol.name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["protocol"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "ip_range", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ip_range"])), - - // Security Group Rule (icmp) - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule_icmp", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule_icmp", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule_icmp", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["direction"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["description_rule"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "ether_type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ether_type"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "icmp_parameters.code", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["icmp_code"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "icmp_parameters.type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["icmp_type"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "protocol.name", "icmp"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "ip_range", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ip_range"])), - - // Security Group Rule (remote) - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule_remote_security_group", "remote_security_group_id", - "stackit_security_group.security_group_remote", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["direction"])), - ), - }, - // Data source - { - ConfigVariables: testConfigSecurityGroupsVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_security_group" "security_group" { - project_id = stackit_security_group.security_group.project_id - security_group_id = stackit_security_group.security_group.security_group_id - } - - data "stackit_security_group" "security_group_remote" { - project_id = stackit_security_group.security_group_remote.project_id - security_group_id = stackit_security_group.security_group_remote.security_group_id - } - - data "stackit_security_group_rule" "security_group_rule" { - project_id = stackit_security_group.security_group.project_id - security_group_id = stackit_security_group.security_group.security_group_id - security_group_rule_id = stackit_security_group_rule.security_group_rule.security_group_rule_id - } - - data "stackit_security_group_rule" "security_group_rule_icmp" { - project_id = stackit_security_group.security_group.project_id - security_group_id = stackit_security_group.security_group.security_group_id - security_group_rule_id = stackit_security_group_rule.security_group_rule_icmp.security_group_rule_id - } - - data "stackit_security_group_rule" "security_group_rule_remote_security_group" { - project_id = stackit_security_group.security_group.project_id - security_group_id = stackit_security_group.security_group.security_group_id - security_group_rule_id = stackit_security_group_rule.security_group_rule_remote_security_group.security_group_rule_id - } - `, - testutil.IaaSProviderConfig(), resourceSecurityGroupMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Security Group (default) - resource.TestCheckResourceAttrPair( - "data.stackit_security_group.security_group", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group.security_group", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_security_group.security_group", "security_group_id"), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "stateful", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["stateful"])), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group", "labels.acc-test", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["label"])), - - // Security Group (remote) - resource.TestCheckResourceAttrPair( - "data.stackit_security_group.security_group_remote", "project_id", - "stackit_security_group.security_group_remote", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group.security_group_remote", "security_group_id", - "stackit_security_group.security_group_remote", "security_group_id", - ), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group_remote", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_security_group.security_group_remote", "security_group_id"), - resource.TestCheckResourceAttr("data.stackit_security_group.security_group_remote", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["name_remote"])), - - // Security Group Rule (default) - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule", "project_id", - "data.stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule", "security_group_id", - "data.stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule", "security_group_rule_id", - "stackit_security_group_rule.security_group_rule", "security_group_rule_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group_rule.security_group_rule", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group_rule.security_group_rule", "security_group_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["direction"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["description_rule"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "ether_type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ether_type"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "port_range.min", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["port"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "port_range.max", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["port"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "protocol.name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["protocol"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "ip_range", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ip_range"])), - - // Security Group Rule (icmp) - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_icmp", "project_id", - "data.stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_icmp", "security_group_id", - "data.stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_icmp", "security_group_rule_id", - "stackit_security_group_rule.security_group_rule_icmp", "security_group_rule_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_icmp", "project_id", - "stackit_security_group_rule.security_group_rule_icmp", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_icmp", "security_group_id", - "stackit_security_group_rule.security_group_rule_icmp", "security_group_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_security_group_rule.security_group_rule_icmp", "security_group_rule_id"), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule_icmp", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["direction"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule_icmp", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["description_rule"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule_icmp", "ether_type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ether_type"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule_icmp", "icmp_parameters.code", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["icmp_code"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule_icmp", "icmp_parameters.type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["icmp_type"])), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule_icmp", "protocol.name", "icmp"), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule_icmp", "ip_range", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["ip_range"])), - - // Security Group Rule (remote) - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_remote_security_group", "project_id", - "data.stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_remote_security_group", "security_group_id", - "data.stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_remote_security_group", "security_group_rule_id", - "stackit_security_group_rule.security_group_rule_remote_security_group", "security_group_rule_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_remote_security_group", "project_id", - "stackit_security_group_rule.security_group_rule_remote_security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_remote_security_group", "security_group_id", - "stackit_security_group_rule.security_group_rule_remote_security_group", "security_group_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_remote_security_group", "remote_security_group_id", - "stackit_security_group_rule.security_group_rule_remote_security_group", "remote_security_group_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_security_group_rule.security_group_rule_remote_security_group", "remote_security_group_id", - "data.stackit_security_group.security_group_remote", "security_group_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("data.stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMax["direction"])), - ), - }, - // Import - { - ConfigVariables: testConfigSecurityGroupsVarsMax, - ResourceName: "stackit_security_group.security_group", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_security_group.security_group"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_security_group.security_group") - } - securityGroupId, ok := r.Primary.Attributes["security_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute security_group_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigSecurityGroupsVarsMax, - ResourceName: "stackit_security_group_rule.security_group_rule", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_security_group_rule.security_group_rule"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_security_group_rule.security_group_rule") - } - securityGroupId, ok := r.Primary.Attributes["security_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute security_group_id") - } - securityGroupRuleId, ok := r.Primary.Attributes["security_group_rule_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute security_group_rule_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, securityGroupId, securityGroupRuleId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigSecurityGroupsVarsMaxUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceSecurityGroupMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Security Group (default) - resource.TestCheckResourceAttr("stackit_security_group.security_group", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group", "security_group_id"), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["description"])), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "stateful", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["stateful"])), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_security_group.security_group", "labels.acc-test", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["label"])), - - // Security Group (remote) - resource.TestCheckResourceAttr("stackit_security_group.security_group_remote", "project_id", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_security_group.security_group_remote", "security_group_id"), - resource.TestCheckResourceAttr("stackit_security_group.security_group_remote", "name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["name_remote"])), - - // Security Group Rule (default) - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["direction"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["description_rule"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "ether_type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["ether_type"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "port_range.min", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["port"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "port_range.max", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["port"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "protocol.name", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["protocol"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "ip_range", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["ip_range"])), - - // Security Group Rule (icmp) - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule_icmp", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule_icmp", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule_icmp", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["direction"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "description", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["description_rule"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "ether_type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["ether_type"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "icmp_parameters.code", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["icmp_code"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "icmp_parameters.type", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["icmp_type"])), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "protocol.name", "icmp"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule_icmp", "ip_range", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["ip_range"])), - - // Security Group Rule (remote) - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "project_id", - "stackit_security_group.security_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule", "security_group_id", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_security_group_rule.security_group_rule_remote_security_group", "remote_security_group_id", - "stackit_security_group.security_group_remote", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_security_group_rule.security_group_rule", "security_group_rule_id"), - resource.TestCheckResourceAttr("stackit_security_group_rule.security_group_rule", "direction", testutil.ConvertConfigVariable(testConfigSecurityGroupsVarsMaxUpdated()["direction"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkInterfaceMin(t *testing.T) { - t.Logf("TestAccNetworkInterfaceMin name: %s", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkInterfaceVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkInterfaceMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network interface instance - resource.TestCheckNoResourceAttr("stackit_network_interface.network_interface", "name"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "ipv4"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "allowed_addresses.#"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "security", "true"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "labels.#", "0"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "security_group_ids.#", "0"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "mac"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "network_interface_id"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "type"), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface", "network_id", - "stackit_network.network", "network_id", - ), - - // Network instance - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - - // Public ip - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip", "public_ip_id"), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip", "ip"), - resource.TestCheckNoResourceAttr("stackit_public_ip.public_ip", "network_interface_id"), - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "labels.%", "0"), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkInterfaceVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network_interface" "network_interface" { - project_id = stackit_network_interface.network_interface.project_id - network_id = stackit_network_interface.network_interface.network_id - network_interface_id = stackit_network_interface.network_interface.network_interface_id - } - - data "stackit_network" "network" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id - } - - data "stackit_public_ip" "public_ip" { - project_id = stackit_public_ip.public_ip.project_id - public_ip_id = stackit_public_ip.public_ip.public_ip_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkInterfaceMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Network interface instance - resource.TestCheckNoResourceAttr("data.stackit_network_interface.network_interface", "name"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface", "ipv4"), - resource.TestCheckNoResourceAttr("data.stackit_network_interface.network_interface", "allowed_addresses.#"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "security", "true"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "labels.#", "0"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "security_group_ids.#", "0"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface", "mac"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface", "network_interface_id"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface", "type"), - - // Network instance - resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["name"])), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), - - // Public ip - resource.TestCheckResourceAttr("data.stackit_public_ip.public_ip", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip", "public_ip_id", - "stackit_public_ip.public_ip", "public_ip_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip", "ip", - "stackit_public_ip.public_ip", "ip", - ), - resource.TestCheckNoResourceAttr("data.stackit_public_ip.public_ip", "network_interface_id"), - resource.TestCheckResourceAttr("data.stackit_public_ip.public_ip", "labels.%", "0"), - ), - }, - - // Import - { - ConfigVariables: testConfigNetworkInterfaceVarsMin, - ResourceName: "stackit_network_interface.network_interface", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_interface.network_interface"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_interface.network_interface") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - networkInterfaceId, ok := r.Primary.Attributes["network_interface_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_interface_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkInterfaceVarsMin, - ResourceName: "stackit_network.network", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkInterfaceVarsMin, - ResourceName: "stackit_public_ip.public_ip", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_public_ip.public_ip"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_public_ip.public_ip") - } - publicIpId, ok := r.Primary.Attributes["public_ip_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute public_ip_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // In this minimal setup, no update can be performed - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccNetworkInterfaceMax(t *testing.T) { - t.Logf("TestAccNetworkInterfaceMax name: %s", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkInterfaceMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network interface instance - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "ipv4", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["ipv4"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "allowed_addresses.#", "1"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "allowed_addresses.0", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["allowed_address"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "security", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["security"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["label"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "security_group_ids.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface", "network_id", - "stackit_network.network", "network_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface", "security_group_ids.0", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "mac"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "network_interface_id"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "type"), - - // Network instance - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - - // Public ip - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip", "public_ip_id"), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip", "ip"), - resource.TestCheckResourceAttrPair( - "stackit_public_ip.public_ip", "network_interface_id", - "stackit_network_interface.network_interface", "network_interface_id", - ), - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["label"])), - - // Network interface simple - resource.TestCheckResourceAttr("stackit_network_interface.network_interface_simple", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_simple", "network_interface_id"), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_simple", "network_id", - "stackit_network.network", "network_id", - ), - - // Public ip simple - resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "public_ip_id"), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "ip"), - resource.TestCheckNoResourceAttr("stackit_public_ip.public_ip_simple", "network_interface_id"), - resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "labels.%", "0"), - - // Nic and public ip attach - resource.TestCheckResourceAttr("stackit_public_ip_associate.nic_public_ip_attach", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_public_ip_associate.nic_public_ip_attach", "public_ip_id", - "stackit_public_ip.public_ip_simple", "public_ip_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_public_ip_associate.nic_public_ip_attach", "network_interface_id", - "stackit_network_interface.network_interface_simple", "network_interface_id", - ), - ), - }, - // Data source - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_network_interface" "network_interface" { - project_id = stackit_network_interface.network_interface.project_id - network_id = stackit_network_interface.network_interface.network_id - network_interface_id = stackit_network_interface.network_interface.network_interface_id - } - - data "stackit_network" "network" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id - } - - data "stackit_public_ip" "public_ip" { - project_id = stackit_public_ip.public_ip.project_id - public_ip_id = stackit_public_ip.public_ip.public_ip_id - } - - data "stackit_network_interface" "network_interface_simple" { - project_id = stackit_network_interface.network_interface_simple.project_id - network_id = stackit_network_interface.network_interface_simple.network_id - network_interface_id = stackit_network_interface.network_interface_simple.network_interface_id - } - - data "stackit_public_ip" "public_ip_simple" { - project_id = stackit_public_ip.public_ip_simple.project_id - public_ip_id = stackit_public_ip.public_ip_simple.public_ip_id - } - `, - testutil.IaaSProviderConfig(), resourceNetworkInterfaceMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Network interface instance - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "data.stackit_network_interface.network_interface", "project_id", - "stackit_network_interface.network_interface", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_network_interface.network_interface", "network_interface_id", - "stackit_network_interface.network_interface", "network_interface_id", - ), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "ipv4", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["ipv4"])), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "allowed_addresses.#", "1"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "allowed_addresses.0", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["allowed_address"])), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "security", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["security"])), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["label"])), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface", "security_group_ids.#", "1"), - resource.TestCheckResourceAttrPair( - "data.stackit_network_interface.network_interface", "security_group_ids.0", - "stackit_security_group.security_group", "security_group_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface", "mac"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface", "network_interface_id"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface", "type"), - - // Network instance - resource.TestCheckResourceAttrSet("data.stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("data.stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["name"])), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("data.stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("data.stackit_network.network", "public_ip"), - - // Public ip - resource.TestCheckResourceAttr("data.stackit_public_ip.public_ip", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip", "public_ip_id", - "stackit_public_ip.public_ip", "public_ip_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip", "ip", - "stackit_public_ip.public_ip", "ip", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip", "network_interface_id", - "data.stackit_network_interface.network_interface", "network_interface_id", - ), - resource.TestCheckResourceAttr("data.stackit_public_ip.public_ip", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_public_ip.public_ip", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["label"])), - - // Network interface simple - resource.TestCheckNoResourceAttr("data.stackit_network_interface.network_interface_simple", "name"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface_simple", "ipv4"), - resource.TestCheckNoResourceAttr("data.stackit_network_interface.network_interface_simple", "allowed_addresses.#"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface_simple", "security", "true"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface_simple", "labels.#", "0"), - resource.TestCheckResourceAttr("data.stackit_network_interface.network_interface_simple", "security_group_ids.#", "0"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface_simple", "mac"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface_simple", "network_interface_id"), - resource.TestCheckResourceAttrSet("data.stackit_network_interface.network_interface_simple", "type"), - - // Public ip simple - resource.TestCheckResourceAttr("data.stackit_public_ip.public_ip_simple", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip_simple", "public_ip_id", - "stackit_public_ip.public_ip_simple", "public_ip_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip_simple", "ip", - "stackit_public_ip.public_ip_simple", "ip", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_public_ip.public_ip_simple", "network_interface_id", - "data.stackit_network_interface.network_interface_simple", "network_interface_id", - ), - resource.TestCheckResourceAttr("data.stackit_public_ip.public_ip_simple", "labels.%", "0"), - ), - }, - // Import - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - ResourceName: "stackit_network_interface.network_interface", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_interface.network_interface"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_interface.network_interface") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - networkInterfaceId, ok := r.Primary.Attributes["network_interface_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_interface_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - ResourceName: "stackit_network.network", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network.network"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network.network") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, networkId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - ResourceName: "stackit_public_ip.public_ip", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_public_ip.public_ip"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_public_ip.public_ip") - } - publicIpId, ok := r.Primary.Attributes["public_ip_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute public_ip_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - ResourceName: "stackit_network_interface.network_interface_simple", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_network_interface.network_interface_simple"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_network_interface.network_interface_simple") - } - networkId, ok := r.Primary.Attributes["network_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_id") - } - networkInterfaceId, ok := r.Primary.Attributes["network_interface_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_interface_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, networkId, networkInterfaceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - ResourceName: "stackit_public_ip.public_ip_simple", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_public_ip.public_ip_simple"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_public_ip.public_ip_simple") - } - publicIpId, ok := r.Primary.Attributes["public_ip_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute public_ip_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigNetworkInterfaceVarsMax, - ResourceName: "stackit_public_ip_associate.nic_public_ip_attach", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_public_ip.public_ip"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_public_ip.public_ip") - } - publicIpId, ok := r.Primary.Attributes["public_ip_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute public_ip_id") - } - networkInterfaceId, ok := r.Primary.Attributes["network_interface_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_interface_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, publicIpId, networkInterfaceId), nil - }, - ImportState: true, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_public_ip_associate.nic_public_ip_attach", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_public_ip_associate.nic_public_ip_attach", "public_ip_id", - "stackit_public_ip.public_ip_simple", "public_ip_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_public_ip_associate.nic_public_ip_attach", "network_interface_id", - "stackit_network_interface.network_interface_simple", "network_interface_id", - ), - ), - }, - // Update - { - ConfigVariables: testConfigNetworkInterfaceVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceNetworkInterfaceMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Network interface instance - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "ipv4", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["ipv4"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "allowed_addresses.#", "0"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "security", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["security"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["label"])), - resource.TestCheckResourceAttr("stackit_network_interface.network_interface", "security_group_ids.#", "0"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "mac"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "network_interface_id"), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface", "type"), - - // Network instance - resource.TestCheckResourceAttrSet("stackit_network.network", "network_id"), - resource.TestCheckResourceAttr("stackit_network.network", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttr("stackit_network.network", "name", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["name"])), - resource.TestCheckResourceAttrSet("stackit_network.network", "ipv4_prefixes.#"), - resource.TestCheckNoResourceAttr("stackit_network.network", "ipv6_prefixes.#"), - resource.TestCheckResourceAttrSet("stackit_network.network", "public_ip"), - - // Public ip - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip", "public_ip_id"), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip", "ip"), - resource.TestCheckResourceAttrPair( - "stackit_public_ip.public_ip", "network_interface_id", - "stackit_network_interface.network_interface", "network_interface_id", - ), - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_public_ip.public_ip", "labels.acc-test", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["label"])), - - // Network interface simple - resource.TestCheckResourceAttr("stackit_network_interface.network_interface_simple", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_network_interface.network_interface_simple", "network_interface_id"), - resource.TestCheckResourceAttrPair( - "stackit_network_interface.network_interface_simple", "network_id", - "stackit_network.network", "network_id", - ), - - // Public ip simple - resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "public_ip_id"), - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "ip"), - // The network gets re-created, which triggers a re-create of the 'network_interface_simple' NIC, which leads the 'stackit_public_ip_associate' resource to update the - // networkInterfaceId of the public IP. All that without the public ip resource noticing. So the public ip resource will still hold the networkInterfaceId of the old NIC. - // So we can only check that *some* network interface ID is set here, but can't compare it with the networkInterfaceId of the NIC resource (old vs. new NIC id) - resource.TestCheckResourceAttrSet("stackit_public_ip.public_ip_simple", "network_interface_id"), - resource.TestCheckResourceAttr("stackit_public_ip.public_ip_simple", "labels.%", "0"), - - // Nic and public ip attach - resource.TestCheckResourceAttr("stackit_public_ip_associate.nic_public_ip_attach", "project_id", testutil.ConvertConfigVariable(testConfigNetworkInterfaceVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_public_ip_associate.nic_public_ip_attach", "public_ip_id", - "stackit_public_ip.public_ip_simple", "public_ip_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_public_ip_associate.nic_public_ip_attach", "network_interface_id", - "stackit_network_interface.network_interface_simple", "network_interface_id", - ), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccKeyPairMin(t *testing.T) { - t.Logf("TestAccKeyPairMin name: %s", testutil.ConvertConfigVariable(testConfigKeyPairMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigKeyPairMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceKeyPairMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigKeyPairMin["name"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigKeyPairMin["public_key"])), - resource.TestCheckResourceAttrSet("stackit_key_pair.key_pair", "fingerprint"), - ), - }, - // Data source - { - ConfigVariables: testConfigKeyPairMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_key_pair" "key_pair" { - name = stackit_key_pair.key_pair.name - } - `, - testutil.IaaSProviderConfig(), resourceKeyPairMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigKeyPairMin["name"])), - resource.TestCheckResourceAttr("data.stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigKeyPairMin["public_key"])), - resource.TestCheckResourceAttrSet("data.stackit_key_pair.key_pair", "fingerprint"), - resource.TestCheckResourceAttrPair( - "stackit_key_pair.key_pair", "fingerprint", - "data.stackit_key_pair.key_pair", "fingerprint", - ), - ), - }, - // Import - { - ConfigVariables: testConfigKeyPairMin, - ResourceName: "stackit_key_pair.key_pair", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_key_pair.key_pair"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_key_pair.key_pair") - } - keyPairName, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return keyPairName, nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // In this minimal setup, no update can be performed - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccKeyPairMax(t *testing.T) { - t.Logf("TestAccKeyPairMax name: %s", testutil.ConvertConfigVariable(testConfigKeyPairMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigKeyPairMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceKeyPairMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigKeyPairMax["name"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigKeyPairMax["public_key"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "labels.acc-test", testutil.ConvertConfigVariable(testConfigKeyPairMax["label"])), - resource.TestCheckResourceAttrSet("stackit_key_pair.key_pair", "fingerprint"), - ), - }, - // Data source - { - ConfigVariables: testConfigKeyPairMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_key_pair" "key_pair" { - name = stackit_key_pair.key_pair.name - } - `, - testutil.IaaSProviderConfig(), resourceKeyPairMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigKeyPairMax["name"])), - resource.TestCheckResourceAttr("data.stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigKeyPairMax["public_key"])), - resource.TestCheckResourceAttr("data.stackit_key_pair.key_pair", "labels.acc-test", testutil.ConvertConfigVariable(testConfigKeyPairMax["label"])), - resource.TestCheckResourceAttrSet("data.stackit_key_pair.key_pair", "fingerprint"), - resource.TestCheckResourceAttrPair( - "stackit_key_pair.key_pair", "fingerprint", - "data.stackit_key_pair.key_pair", "fingerprint", - ), - ), - }, - // Import - { - ConfigVariables: testConfigKeyPairMax, - ResourceName: "stackit_key_pair.key_pair", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_key_pair.key_pair"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_key_pair.key_pair") - } - keyPairName, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return keyPairName, nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigKeyPairMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceKeyPairMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", testutil.ConvertConfigVariable(testConfigKeyPairMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "public_key", testutil.ConvertConfigVariable(testConfigKeyPairMaxUpdated["public_key"])), - resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "labels.acc-test", testutil.ConvertConfigVariable(testConfigKeyPairMaxUpdated["label"])), - resource.TestCheckResourceAttrSet("stackit_key_pair.key_pair", "fingerprint"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccImageMin(t *testing.T) { - t.Logf("TestAccImageMin name: %s", testutil.ConvertConfigVariable(testConfigImageVarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - - // Creation - { - ConfigVariables: testConfigImageVarsMin, - Config: fmt.Sprintf("%s\n%s", resourceImageMinConfig, testutil.IaaSProviderConfig()), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_image.image", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "image_id"), - resource.TestCheckResourceAttr("stackit_image.image", "name", testutil.ConvertConfigVariable(testConfigImageVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_image.image", "disk_format", testutil.ConvertConfigVariable(testConfigImageVarsMin["disk_format"])), - resource.TestCheckResourceAttr("stackit_image.image", "local_file_path", testutil.ConvertConfigVariable(testConfigImageVarsMin["local_file_path"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "protected"), - resource.TestCheckResourceAttrSet("stackit_image.image", "scope"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - ), - }, - // Data source - { - ConfigVariables: testConfigImageVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_image" "image" { - project_id = stackit_image.image.project_id - image_id = stackit_image.image.image_id - } - `, - resourceImageMinConfig, testutil.IaaSProviderConfig(), - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_image.image", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMin["project_id"])), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "image_id", "stackit_image.image", "image_id"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "name", "stackit_image.image", "name"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "disk_format", "stackit_image.image", "disk_format"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "min_disk_size", "stackit_image.image", "min_disk_size"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "min_ram", "stackit_image.image", "min_ram"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "protected", "stackit_image.image", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.digest"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.digest"), - ), - }, - // Import - { - ConfigVariables: testConfigImageVarsMin, - ResourceName: "stackit_image.image", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_image.image"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_image.image") - } - imageId, ok := r.Primary.Attributes["image_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute image_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, imageId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"local_file_path"}, - }, - // Update - { - ConfigVariables: testConfigImageVarsMinUpdated, - Config: fmt.Sprintf("%s\n%s", resourceImageMinConfig, testutil.IaaSProviderConfig()), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_image.image", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMinUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "image_id"), - resource.TestCheckResourceAttr("stackit_image.image", "name", testutil.ConvertConfigVariable(testConfigImageVarsMinUpdated["name"])), - resource.TestCheckResourceAttr("stackit_image.image", "disk_format", testutil.ConvertConfigVariable(testConfigImageVarsMinUpdated["disk_format"])), - resource.TestCheckResourceAttr("stackit_image.image", "local_file_path", testutil.ConvertConfigVariable(testConfigImageVarsMinUpdated["local_file_path"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "protected"), - resource.TestCheckResourceAttrSet("stackit_image.image", "scope"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccImageMax(t *testing.T) { - t.Logf("TestAccImageMax name: %s", testutil.ConvertConfigVariable(testConfigImageVarsMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - - // Creation - { - ConfigVariables: testConfigImageVarsMax, - Config: fmt.Sprintf("%s\n%s", resourceImageMaxConfig, testutil.IaaSProviderConfig()), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_image.image", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "image_id"), - resource.TestCheckResourceAttr("stackit_image.image", "name", testutil.ConvertConfigVariable(testConfigImageVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_image.image", "disk_format", testutil.ConvertConfigVariable(testConfigImageVarsMax["disk_format"])), - resource.TestCheckResourceAttr("stackit_image.image", "local_file_path", testutil.ConvertConfigVariable(testConfigImageVarsMax["local_file_path"])), - resource.TestCheckResourceAttr("stackit_image.image", "min_disk_size", testutil.ConvertConfigVariable(testConfigImageVarsMax["min_disk_size"])), - resource.TestCheckResourceAttr("stackit_image.image", "min_ram", testutil.ConvertConfigVariable(testConfigImageVarsMax["min_ram"])), - resource.TestCheckResourceAttr("stackit_image.image", "labels.acc-test", testutil.ConvertConfigVariable(testConfigImageVarsMax["label"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.boot_menu", testutil.ConvertConfigVariable(testConfigImageVarsMax["boot_menu"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.cdrom_bus", testutil.ConvertConfigVariable(testConfigImageVarsMax["cdrom_bus"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.disk_bus", testutil.ConvertConfigVariable(testConfigImageVarsMax["disk_bus"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.nic_model", testutil.ConvertConfigVariable(testConfigImageVarsMax["nic_model"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.operating_system", testutil.ConvertConfigVariable(testConfigImageVarsMax["operating_system"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.operating_system_distro", testutil.ConvertConfigVariable(testConfigImageVarsMax["operating_system_distro"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.operating_system_version", testutil.ConvertConfigVariable(testConfigImageVarsMax["operating_system_version"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.rescue_bus", testutil.ConvertConfigVariable(testConfigImageVarsMax["rescue_bus"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.rescue_device", testutil.ConvertConfigVariable(testConfigImageVarsMax["rescue_device"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.secure_boot", testutil.ConvertConfigVariable(testConfigImageVarsMax["secure_boot"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.uefi", testutil.ConvertConfigVariable(testConfigImageVarsMax["uefi"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.video_model", testutil.ConvertConfigVariable(testConfigImageVarsMax["video_model"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.virtio_scsi", testutil.ConvertConfigVariable(testConfigImageVarsMax["virtio_scsi"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "protected"), - resource.TestCheckResourceAttrSet("stackit_image.image", "scope"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - ), - }, - // Data source - { - ConfigVariables: testConfigImageVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_image" "image" { - project_id = stackit_image.image.project_id - image_id = stackit_image.image.image_id - } - `, - resourceImageMaxConfig, testutil.IaaSProviderConfig(), - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_image.image", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "image_id", "stackit_image.image", "image_id"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "name", "stackit_image.image", "name"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "disk_format", "stackit_image.image", "disk_format"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "min_disk_size", "stackit_image.image", "min_disk_size"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "min_ram", "stackit_image.image", "min_ram"), - resource.TestCheckResourceAttrPair("data.stackit_image.image", "protected", "stackit_image.image", "protected"), - resource.TestCheckResourceAttr("data.stackit_image.image", "min_disk_size", testutil.ConvertConfigVariable(testConfigImageVarsMax["min_disk_size"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "min_ram", testutil.ConvertConfigVariable(testConfigImageVarsMax["min_ram"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "labels.acc-test", testutil.ConvertConfigVariable(testConfigImageVarsMax["label"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.boot_menu", testutil.ConvertConfigVariable(testConfigImageVarsMax["boot_menu"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.cdrom_bus", testutil.ConvertConfigVariable(testConfigImageVarsMax["cdrom_bus"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.disk_bus", testutil.ConvertConfigVariable(testConfigImageVarsMax["disk_bus"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.nic_model", testutil.ConvertConfigVariable(testConfigImageVarsMax["nic_model"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.operating_system", testutil.ConvertConfigVariable(testConfigImageVarsMax["operating_system"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.operating_system_distro", testutil.ConvertConfigVariable(testConfigImageVarsMax["operating_system_distro"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.operating_system_version", testutil.ConvertConfigVariable(testConfigImageVarsMax["operating_system_version"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.rescue_bus", testutil.ConvertConfigVariable(testConfigImageVarsMax["rescue_bus"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.rescue_device", testutil.ConvertConfigVariable(testConfigImageVarsMax["rescue_device"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.secure_boot", testutil.ConvertConfigVariable(testConfigImageVarsMax["secure_boot"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.uefi", testutil.ConvertConfigVariable(testConfigImageVarsMax["uefi"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.video_model", testutil.ConvertConfigVariable(testConfigImageVarsMax["video_model"])), - resource.TestCheckResourceAttr("data.stackit_image.image", "config.virtio_scsi", testutil.ConvertConfigVariable(testConfigImageVarsMax["virtio_scsi"])), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.digest"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image.image", "checksum.digest"), - ), - }, - // Import - { - ConfigVariables: testConfigImageVarsMax, - ResourceName: "stackit_image.image", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_image.image"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_image.image") - } - imageId, ok := r.Primary.Attributes["image_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute image_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, imageId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"local_file_path"}, - }, - // Update - { - ConfigVariables: testConfigImageVarsMaxUpdated, - Config: fmt.Sprintf("%s\n%s", resourceImageMaxConfig, testutil.IaaSProviderConfig()), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_image.image", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["project_id"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "image_id"), - resource.TestCheckResourceAttr("stackit_image.image", "name", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_image.image", "disk_format", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["disk_format"])), - resource.TestCheckResourceAttr("stackit_image.image", "local_file_path", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["local_file_path"])), - resource.TestCheckResourceAttr("stackit_image.image", "min_disk_size", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["min_disk_size"])), - resource.TestCheckResourceAttr("stackit_image.image", "min_ram", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["min_ram"])), - resource.TestCheckResourceAttr("stackit_image.image", "labels.acc-test", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["label"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.boot_menu", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["boot_menu"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.cdrom_bus", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["cdrom_bus"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.disk_bus", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["disk_bus"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.nic_model", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["nic_model"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.operating_system", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["operating_system"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.operating_system_distro", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["operating_system_distro"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.operating_system_version", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["operating_system_version"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.rescue_bus", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["rescue_bus"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.rescue_device", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["rescue_device"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.secure_boot", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["secure_boot"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.uefi", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["uefi"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.video_model", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["video_model"])), - resource.TestCheckResourceAttr("stackit_image.image", "config.virtio_scsi", testutil.ConvertConfigVariable(testConfigImageVarsMaxUpdated["virtio_scsi"])), - resource.TestCheckResourceAttrSet("stackit_image.image", "protected"), - resource.TestCheckResourceAttrSet("stackit_image.image", "scope"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("stackit_image.image", "checksum.digest"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccImageDatasourceSearchVariants(t *testing.T) { - t.Log("TestDataSource Image Variants") - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: config.Variables{"project_id": config.StringVariable(testutil.ProjectId)}, - Config: fmt.Sprintf("%s\n%s", dataSourceImageVariants, testutil.IaaSProviderConfigWithBetaResourcesEnabled()), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_image_v2.name_match_ubuntu_22_04", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_match_ubuntu_22_04", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.ubuntu_by_image_id", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_by_image_id", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.regex_match_ubuntu_22_04", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.regex_match_ubuntu_22_04", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.filter_debian_11", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_debian_11", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.filter_uefi_ubuntu", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.filter_uefi_ubuntu", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_regex_and_filter_rhel_9_1", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.name_windows_2022_standard", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.name_windows_2022_standard", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.ubuntu_arm64_latest", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_latest", "checksum.digest"), - - resource.TestCheckResourceAttr("data.stackit_image_v2.ubuntu_arm64_oldest", "project_id", testutil.ConvertConfigVariable(testConfigImageVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "image_id"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "name"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "min_disk_size"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "min_ram"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "protected"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "scope"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "checksum.algorithm"), - resource.TestCheckResourceAttrSet("data.stackit_image_v2.ubuntu_arm64_oldest", "checksum.digest"), - - // e2e test that ascending sort is working - func(s *terraform.State) error { - latest := s.RootModule().Resources["data.stackit_image_v2.ubuntu_arm64_latest"] - oldest := s.RootModule().Resources["data.stackit_image_v2.ubuntu_arm64_oldest"] - - if latest == nil { - return fmt.Errorf("datasource 'data.stackit_image_v2.ubuntu_arm64_latest' not found") - } - if oldest == nil { - return fmt.Errorf("datasource 'data.stackit_image_v2.ubuntu_arm64_oldest' not found") - } - - nameLatest := latest.Primary.Attributes["name"] - nameOldest := oldest.Primary.Attributes["name"] - - if nameLatest == nameOldest { - return fmt.Errorf("expected image names to differ, but both are %q", nameLatest) - } - - return nil - }, - ), - }, - }, - }) -} - -func TestAccDatasourcePublicIpRanges(t *testing.T) { - t.Log("TestDataSource STACKIT Public Ip Ranges") - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - // Read - { - ConfigVariables: config.Variables{}, - Config: fmt.Sprintf("%s\n%s", datasourcePublicIpRanges, testutil.IaaSProviderConfig()), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet("data.stackit_public_ip_ranges.example", "id"), - resource.TestCheckResourceAttrSet("data.stackit_public_ip_ranges.example", "public_ip_ranges.0.cidr"), - resource.TestCheckResourceAttrSet("data.stackit_public_ip_ranges.example", "cidr_list.0"), - ), - }, - }, - }) -} - -func TestAccProject(t *testing.T) { - projectId := testutil.ProjectId - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - // Data source - { - ConfigVariables: testConfigKeyPairMin, - Config: fmt.Sprintf(` - %s - - data "stackit_iaas_project" "project" { - project_id = %q - } - `, - testutil.IaaSProviderConfig(), testutil.ProjectId, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_iaas_project.project", "project_id", projectId), - resource.TestCheckResourceAttr("data.stackit_iaas_project.project", "id", projectId), - resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "area_id"), - resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "internet_access"), - resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "state"), - resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "status"), - resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_iaas_project.project", "updated_at"), - ), - }, - }, - }) -} - -func TestAccMachineType(t *testing.T) { - t.Logf("TestAccMachineType projectid: %s", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - Steps: []resource.TestStep{ - { - ConfigVariables: testConfigMachineTypeVars, - Config: fmt.Sprintf("%s\n%s", dataSourceMachineTypeConfig, testutil.IaaSProviderConfigWithBetaResourcesEnabled()), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_machine_type.two_vcpus_filter", "project_id", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "id"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "name"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "vcpus"), - resource.TestCheckResourceAttr("data.stackit_machine_type.two_vcpus_filter", "vcpus", "2"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "ram"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "disk"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "description"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "extra_specs.cpu"), - - resource.TestCheckResourceAttr("data.stackit_machine_type.filter_sorted_ascending_false", "project_id", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "id"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "name"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "vcpus"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "ram"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "disk"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "description"), - resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "extra_specs.cpu"), - - resource.TestCheckResourceAttr("data.stackit_machine_type.no_match", "project_id", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])), - resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "description"), - resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "disk"), - resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "extra_specs"), - resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "id"), - resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "name"), - resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "ram"), - ), - }, - }, - }) -} - -func testAccCheckDestroy(s *terraform.State) error { - checkFunctions := []func(s *terraform.State) error{ - testAccCheckIaaSVolumeDestroy, - testAccCheckServerDestroy, - testAccCheckAffinityGroupDestroy, - testAccCheckIaaSSecurityGroupDestroy, - testAccCheckIaaSPublicIpDestroy, - testAccCheckIaaSKeyPairDestroy, - testAccCheckIaaSImageDestroy, - testAccCheckNetworkDestroy, - testAccCheckNetworkInterfaceDestroy, - testAccCheckNetworkAreaRegionDestroy, - testAccCheckNetworkAreaDestroy, - } - var errs []error - - wg := sync.WaitGroup{} - wg.Add(len(checkFunctions)) - - for _, f := range checkFunctions { - go func() { - err := f(s) - if err != nil { - errs = append(errs, err) - } - wg.Done() - }() - } - wg.Wait() - return errors.Join(errs...) -} - -func testAccCheckNetworkDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaasalpha.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaasalpha.NewAPIClient() - } else { - client, err = iaasalpha.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - // networks - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network" { - continue - } - region := strings.Split(rs.Primary.ID, core.Separator)[1] - networkId := strings.Split(rs.Primary.ID, core.Separator)[2] - err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, region, networkId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger network deletion %q: %w", networkId, err)) - } - _, err = waitAlpha.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, region, networkId).WaitWithContext(ctx) - if err != nil { - errs = append(errs, fmt.Errorf("cannot delete network %q: %w", networkId, err)) - } - } - - return errors.Join(errs...) -} - -func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - // network interfaces - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network_interface" { - continue - } - ids := strings.Split(rs.Primary.ID, core.Separator) - region := ids[1] - networkId := ids[2] - networkInterfaceId := ids[3] - err := client.DeleteNicExecute(ctx, testutil.ProjectId, region, networkId, networkInterfaceId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger network interface deletion %q: %w", networkInterfaceId, err)) - } - if err != nil { - errs = append(errs, fmt.Errorf("cannot delete network interface %q: %w", networkInterfaceId, err)) - } - } - - return errors.Join(errs...) -} - -func testAccCheckNetworkAreaRegionDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - // network areas - networkAreasToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network_area_region" { - continue - } - networkAreaId := strings.Split(rs.Primary.ID, core.Separator)[1] - networkAreasToDestroy = append(networkAreasToDestroy, networkAreaId) - } - - networkAreasResp, err := client.ListNetworkAreasExecute(ctx, testutil.OrganizationId) - if err != nil { - return fmt.Errorf("getting networkAreasResp: %w", err) - } - - networkAreas := *networkAreasResp.Items - for i := range networkAreas { - if networkAreas[i].Id == nil { - continue - } - if utils.Contains(networkAreasToDestroy, *networkAreas[i].Id) { - err := client.DeleteNetworkAreaRegionExecute(ctx, testutil.OrganizationId, *networkAreas[i].Id, testutil.Region) - if err != nil { - return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].Id, err) - } - } - } - return nil -} - -func testAccCheckNetworkAreaDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - // network areas - networkAreasToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network_area" { - continue - } - networkAreaId := strings.Split(rs.Primary.ID, core.Separator)[1] - networkAreasToDestroy = append(networkAreasToDestroy, networkAreaId) - } - - networkAreasResp, err := client.ListNetworkAreasExecute(ctx, testutil.OrganizationId) - if err != nil { - return fmt.Errorf("getting networkAreasResp: %w", err) - } - - networkAreas := *networkAreasResp.Items - for i := range networkAreas { - if networkAreas[i].Id == nil { - continue - } - if utils.Contains(networkAreasToDestroy, *networkAreas[i].Id) { - err := client.DeleteNetworkAreaExecute(ctx, testutil.OrganizationId, *networkAreas[i].Id) - if err != nil { - return fmt.Errorf("destroying network area %s during CheckDestroy: %w", *networkAreas[i].Id, err) - } - } - } - return nil -} - -func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - volumesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_volume" { - continue - } - // volume terraform ID: "[project_id],[volume_id]" - volumeId := strings.Split(rs.Primary.ID, core.Separator)[1] - volumesToDestroy = append(volumesToDestroy, volumeId) - } - - volumesResp, err := client.ListVolumesExecute(ctx, testutil.ProjectId, testutil.Region) - if err != nil { - return fmt.Errorf("getting volumesResp: %w", err) - } - - volumes := *volumesResp.Items - for i := range volumes { - if volumes[i].Id == nil { - continue - } - if utils.Contains(volumesToDestroy, *volumes[i].Id) { - err := client.DeleteVolumeExecute(ctx, testutil.ProjectId, testutil.Region, *volumes[i].Id) - if err != nil { - return fmt.Errorf("destroying volume %s during CheckDestroy: %w", *volumes[i].Id, err) - } - } - } - return nil -} - -func testAccCheckServerDestroy(s *terraform.State) error { - ctx := context.Background() - var alphaClient *iaas.APIClient - var client *iaas.APIClient - var err error - var alphaErr error - if testutil.IaaSCustomEndpoint == "" { - alphaClient, alphaErr = iaas.NewAPIClient() - client, err = iaas.NewAPIClient() - } else { - alphaClient, alphaErr = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - client, err = iaas.NewAPIClient() - } - if err != nil || alphaErr != nil { - return fmt.Errorf("creating client: %w, %w", err, alphaErr) - } - - // Servers - - serversToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_server" { - continue - } - // server terraform ID: "[project_id],[region],[server_id]" - serverId := strings.Split(rs.Primary.ID, core.Separator)[2] - serversToDestroy = append(serversToDestroy, serverId) - } - - serversResp, err := alphaClient.ListServersExecute(ctx, testutil.ProjectId, testutil.Region) - if err != nil { - return fmt.Errorf("getting serversResp: %w", err) - } - - servers := *serversResp.Items - for i := range servers { - if servers[i].Id == nil { - continue - } - if utils.Contains(serversToDestroy, *servers[i].Id) { - err := alphaClient.DeleteServerExecute(ctx, testutil.ProjectId, testutil.Region, *servers[i].Id) - if err != nil { - return fmt.Errorf("destroying server %s during CheckDestroy: %w", *servers[i].Id, err) - } - } - } - - // Networks - - networksToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_network" { - continue - } - // network terraform ID: "[project_id],[network_id]" - networkId := strings.Split(rs.Primary.ID, core.Separator)[1] - networksToDestroy = append(networksToDestroy, networkId) - } - - networksResp, err := client.ListNetworksExecute(ctx, testutil.ProjectId, testutil.Region) - if err != nil { - return fmt.Errorf("getting networksResp: %w", err) - } - - networks := *networksResp.Items - for i := range networks { - if networks[i].Id == nil { - continue - } - if utils.Contains(networksToDestroy, *networks[i].Id) { - err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, testutil.Region, *networks[i].Id) - if err != nil { - return fmt.Errorf("destroying network %s during CheckDestroy: %w", *networks[i].Id, err) - } - } - } - - return nil -} - -func testAccCheckAffinityGroupDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - affinityGroupsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_affinity_group" { - continue - } - // affinity group terraform ID: "[project_id],[region],[affinity_group_id]" - affinityGroupId := strings.Split(rs.Primary.ID, core.Separator)[2] - affinityGroupsToDestroy = append(affinityGroupsToDestroy, affinityGroupId) - } - - affinityGroupsResp, err := client.ListAffinityGroupsExecute(ctx, testutil.ProjectId, testutil.Region) - if err != nil { - return fmt.Errorf("getting securityGroupsResp: %w", err) - } - - affinityGroups := *affinityGroupsResp.Items - for i := range affinityGroups { - if affinityGroups[i].Id == nil { - continue - } - if utils.Contains(affinityGroupsToDestroy, *affinityGroups[i].Id) { - err := client.DeleteAffinityGroupExecute(ctx, testutil.ProjectId, testutil.Region, *affinityGroups[i].Id) - if err != nil { - return fmt.Errorf("destroying affinity group %s during CheckDestroy: %w", *affinityGroups[i].Id, err) - } - } - } - return nil -} - -func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - securityGroupsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_security_group" { - continue - } - // security group terraform ID: "[project_id],[region],[security_group_id]" - securityGroupId := strings.Split(rs.Primary.ID, core.Separator)[2] - securityGroupsToDestroy = append(securityGroupsToDestroy, securityGroupId) - } - - securityGroupsResp, err := client.ListSecurityGroupsExecute(ctx, testutil.ProjectId, testutil.Region) - if err != nil { - return fmt.Errorf("getting securityGroupsResp: %w", err) - } - - securityGroups := *securityGroupsResp.Items - for i := range securityGroups { - if securityGroups[i].Id == nil { - continue - } - if utils.Contains(securityGroupsToDestroy, *securityGroups[i].Id) { - err := client.DeleteSecurityGroupExecute(ctx, testutil.ProjectId, testutil.Region, *securityGroups[i].Id) - if err != nil { - return fmt.Errorf("destroying security group %s during CheckDestroy: %w", *securityGroups[i].Id, err) - } - } - } - return nil -} - -func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - publicIpsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_public_ip" { - continue - } - // public IP terraform ID: "[project_id],[region],[public_ip_id]" - publicIpId := strings.Split(rs.Primary.ID, core.Separator)[2] - publicIpsToDestroy = append(publicIpsToDestroy, publicIpId) - } - - publicIpsResp, err := client.ListPublicIPsExecute(ctx, testutil.ProjectId, testutil.Region) - if err != nil { - return fmt.Errorf("getting publicIpsResp: %w", err) - } - - publicIps := *publicIpsResp.Items - for i := range publicIps { - if publicIps[i].Id == nil { - continue - } - if utils.Contains(publicIpsToDestroy, *publicIps[i].Id) { - err := client.DeletePublicIPExecute(ctx, testutil.ProjectId, testutil.Region, *publicIps[i].Id) - if err != nil { - return fmt.Errorf("destroying public IP %s during CheckDestroy: %w", *publicIps[i].Id, err) - } - } - } - return nil -} - -func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - keyPairsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_key_pair" { - continue - } - // Key pair terraform ID: "[name]" - keyPairsToDestroy = append(keyPairsToDestroy, rs.Primary.ID) - } - - keyPairsResp, err := client.ListKeyPairsExecute(ctx) - if err != nil { - return fmt.Errorf("getting key pairs: %w", err) - } - - keyPairs := *keyPairsResp.Items - for i := range keyPairs { - if keyPairs[i].Name == nil { - continue - } - if utils.Contains(keyPairsToDestroy, *keyPairs[i].Name) { - err := client.DeleteKeyPairExecute(ctx, *keyPairs[i].Name) - if err != nil { - return fmt.Errorf("destroying key pair %s during CheckDestroy: %w", *keyPairs[i].Name, err) - } - } - } - return nil -} - -func testAccCheckIaaSImageDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaas.APIClient - var err error - - if testutil.IaaSCustomEndpoint == "" { - client, err = iaas.NewAPIClient() - } else { - client, err = iaas.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - imagesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_image" { - continue - } - // Image terraform ID: "[project_id],[region],[image_id]" - imageId := strings.Split(rs.Primary.ID, core.Separator)[2] - imagesToDestroy = append(imagesToDestroy, imageId) - } - - imagesResp, err := client.ListImagesExecute(ctx, testutil.ProjectId, testutil.Region) - if err != nil { - return fmt.Errorf("getting images: %w", err) - } - - images := *imagesResp.Items - for i := range images { - if images[i].Id == nil { - continue - } - if utils.Contains(imagesToDestroy, *images[i].Id) { - err := client.DeleteImageExecute(ctx, testutil.ProjectId, testutil.Region, *images[i].Id) - if err != nil { - return fmt.Errorf("destroying image %s during CheckDestroy: %w", *images[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/iaas/image/datasource.go b/stackit/internal/services/iaas/image/datasource.go deleted file mode 100644 index 0d3e1aa2..00000000 --- a/stackit/internal/services/iaas/image/datasource.go +++ /dev/null @@ -1,361 +0,0 @@ -package image - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &imageDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ImageId types.String `tfsdk:"image_id"` - Name types.String `tfsdk:"name"` - DiskFormat types.String `tfsdk:"disk_format"` - MinDiskSize types.Int64 `tfsdk:"min_disk_size"` - MinRAM types.Int64 `tfsdk:"min_ram"` - Protected types.Bool `tfsdk:"protected"` - Scope types.String `tfsdk:"scope"` - Config types.Object `tfsdk:"config"` - Checksum types.Object `tfsdk:"checksum"` - Labels types.Map `tfsdk:"labels"` -} - -// NewImageDataSource is a helper function to simplify the provider implementation. -func NewImageDataSource() datasource.DataSource { - return &imageDataSource{} -} - -// imageDataSource is the data source implementation. -type imageDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *imageDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_image" -} - -func (d *imageDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the datasource. -func (d *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Image datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the image is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "image_id": schema.StringAttribute{ - Description: "The image ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the image.", - Computed: true, - }, - "disk_format": schema.StringAttribute{ - Description: "The disk format of the image.", - Computed: true, - }, - "min_disk_size": schema.Int64Attribute{ - Description: "The minimum disk size of the image in GB.", - Computed: true, - }, - "min_ram": schema.Int64Attribute{ - Description: "The minimum RAM of the image in MB.", - Computed: true, - }, - "protected": schema.BoolAttribute{ - Description: "Whether the image is protected.", - Computed: true, - }, - "scope": schema.StringAttribute{ - Description: "The scope of the image.", - Computed: true, - }, - "config": schema.SingleNestedAttribute{ - Description: "Properties to set hardware and scheduling settings for an image.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "boot_menu": schema.BoolAttribute{ - Description: "Enables the BIOS bootmenu.", - Computed: true, - }, - "cdrom_bus": schema.StringAttribute{ - Description: "Sets CDROM bus controller type.", - Computed: true, - }, - "disk_bus": schema.StringAttribute{ - Description: "Sets Disk bus controller type.", - Computed: true, - }, - "nic_model": schema.StringAttribute{ - Description: "Sets virtual network interface model.", - Computed: true, - }, - "operating_system": schema.StringAttribute{ - Description: "Enables operating system specific optimizations.", - Computed: true, - }, - "operating_system_distro": schema.StringAttribute{ - Description: "Operating system distribution.", - Computed: true, - }, - "operating_system_version": schema.StringAttribute{ - Description: "Version of the operating system.", - Computed: true, - }, - "rescue_bus": schema.StringAttribute{ - Description: "Sets the device bus when the image is used as a rescue image.", - Computed: true, - }, - "rescue_device": schema.StringAttribute{ - Description: "Sets the device when the image is used as a rescue image.", - Computed: true, - }, - "secure_boot": schema.BoolAttribute{ - Description: "Enables Secure Boot.", - Computed: true, - }, - "uefi": schema.BoolAttribute{ - Description: "Enables UEFI boot.", - Computed: true, - }, - "video_model": schema.StringAttribute{ - Description: "Sets Graphic device model.", - Computed: true, - }, - "virtio_scsi": schema.BoolAttribute{ - Description: "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.", - Computed: true, - }, - }, - }, - "checksum": schema.SingleNestedAttribute{ - Description: "Representation of an image checksum.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "algorithm": schema.StringAttribute{ - Description: "Algorithm for the checksum of the image data.", - Computed: true, - }, - "digest": schema.StringAttribute{ - Description: "Hexdigest of the checksum of the image data.", - Computed: true, - }, - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - imageId := model.ImageId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "image_id", imageId) - - imageResp, err := d.client.GetImage(ctx, projectId, region, imageId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading image", - fmt.Sprintf("Image with ID %q does not exist in project %q.", imageId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapDataSourceFields(ctx, imageResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "image read") -} - -func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel, region string) error { - if imageResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var imageId string - if model.ImageId.ValueString() != "" { - imageId = model.ImageId.ValueString() - } else if imageResp.Id != nil { - imageId = *imageResp.Id - } else { - return fmt.Errorf("image id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId) - model.Region = types.StringValue(region) - - // Map config - var configModel = &configModel{} - var configObject basetypes.ObjectValue - diags := diag.Diagnostics{} - if imageResp.Config != nil { - configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu) - configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus()) - configModel.DiskBus = types.StringPointerValue(imageResp.Config.GetDiskBus()) - configModel.NICModel = types.StringPointerValue(imageResp.Config.GetNicModel()) - configModel.OperatingSystem = types.StringPointerValue(imageResp.Config.OperatingSystem) - configModel.OperatingSystemDistro = types.StringPointerValue(imageResp.Config.GetOperatingSystemDistro()) - configModel.OperatingSystemVersion = types.StringPointerValue(imageResp.Config.GetOperatingSystemVersion()) - configModel.RescueBus = types.StringPointerValue(imageResp.Config.GetRescueBus()) - configModel.RescueDevice = types.StringPointerValue(imageResp.Config.GetRescueDevice()) - configModel.SecureBoot = types.BoolPointerValue(imageResp.Config.SecureBoot) - configModel.UEFI = types.BoolPointerValue(imageResp.Config.Uefi) - configModel.VideoModel = types.StringPointerValue(imageResp.Config.GetVideoModel()) - configModel.VirtioScsi = types.BoolPointerValue(iaas.PtrBool(imageResp.Config.GetVirtioScsi())) - - configObject, diags = types.ObjectValue(configTypes, map[string]attr.Value{ - "boot_menu": configModel.BootMenu, - "cdrom_bus": configModel.CDROMBus, - "disk_bus": configModel.DiskBus, - "nic_model": configModel.NICModel, - "operating_system": configModel.OperatingSystem, - "operating_system_distro": configModel.OperatingSystemDistro, - "operating_system_version": configModel.OperatingSystemVersion, - "rescue_bus": configModel.RescueBus, - "rescue_device": configModel.RescueDevice, - "secure_boot": configModel.SecureBoot, - "uefi": configModel.UEFI, - "video_model": configModel.VideoModel, - "virtio_scsi": configModel.VirtioScsi, - }) - } else { - configObject = types.ObjectNull(configTypes) - } - if diags.HasError() { - return fmt.Errorf("creating config: %w", core.DiagsToError(diags)) - } - - // Map checksum - var checksumModel = &checksumModel{} - var checksumObject basetypes.ObjectValue - if imageResp.Checksum != nil { - checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm) - checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest) - checksumObject, diags = types.ObjectValue(checksumTypes, map[string]attr.Value{ - "algorithm": checksumModel.Algorithm, - "digest": checksumModel.Digest, - }) - } else { - checksumObject = types.ObjectNull(checksumTypes) - } - if diags.HasError() { - return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags)) - } - - // Map labels - labels, err := iaasUtils.MapLabels(ctx, imageResp.Labels, model.Labels) - if err != nil { - return err - } - - model.ImageId = types.StringValue(imageId) - model.Name = types.StringPointerValue(imageResp.Name) - model.DiskFormat = types.StringPointerValue(imageResp.DiskFormat) - model.MinDiskSize = types.Int64PointerValue(imageResp.MinDiskSize) - model.MinRAM = types.Int64PointerValue(imageResp.MinRam) - model.Protected = types.BoolPointerValue(imageResp.Protected) - model.Scope = types.StringPointerValue(imageResp.Scope) - model.Labels = labels - model.Config = configObject - model.Checksum = checksumObject - return nil -} diff --git a/stackit/internal/services/iaas/image/datasource_test.go b/stackit/internal/services/iaas/image/datasource_test.go deleted file mode 100644 index 37d81235..00000000 --- a/stackit/internal/services/iaas/image/datasource_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package image - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapDataSourceFields(t *testing.T) { - type args struct { - state DataSourceModel - input *iaas.Image - region string - } - tests := []struct { - description string - args args - expected DataSourceModel - isValid bool - }{ - { - description: "default_values", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu01,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - DiskFormat: utils.Ptr("format"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Scope: utils.Ptr("scope"), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("model")), - VirtioScsi: utils.Ptr(true), - }, - Checksum: &iaas.ImageChecksum{ - Algorithm: utils.Ptr("algorithm"), - Digest: utils.Ptr("digest"), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - region: "eu02", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu02,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Name: types.StringValue("name"), - DiskFormat: types.StringValue("format"), - MinDiskSize: types.Int64Value(1), - MinRAM: types.Int64Value(1), - Protected: types.BoolValue(true), - Scope: types.StringValue("scope"), - Config: types.ObjectValueMust(configTypes, map[string]attr.Value{ - "boot_menu": types.BoolValue(true), - "cdrom_bus": types.StringValue("cdrom_bus"), - "disk_bus": types.StringValue("disk_bus"), - "nic_model": types.StringValue("model"), - "operating_system": types.StringValue("os"), - "operating_system_distro": types.StringValue("os_distro"), - "operating_system_version": types.StringValue("os_version"), - "rescue_bus": types.StringValue("rescue_bus"), - "rescue_device": types.StringValue("rescue_device"), - "secure_boot": types.BoolValue(true), - "uefi": types.BoolValue(true), - "video_model": types.StringValue("model"), - "virtio_scsi": types.BoolValue(true), - }), - Checksum: types.ObjectValueMust(checksumTypes, map[string]attr.Value{ - "algorithm": types.StringValue("algorithm"), - "digest": types.StringValue("digest"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu01,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.Image{}, - }, - expected: DataSourceModel{}, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/image/resource.go b/stackit/internal/services/iaas/image/resource.go deleted file mode 100644 index 0699093f..00000000 --- a/stackit/internal/services/iaas/image/resource.go +++ /dev/null @@ -1,891 +0,0 @@ -package image - -import ( - "bufio" - "context" - "fmt" - "net/http" - "os" - "strings" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &imageResource{} - _ resource.ResourceWithConfigure = &imageResource{} - _ resource.ResourceWithImportState = &imageResource{} - _ resource.ResourceWithModifyPlan = &imageResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ImageId types.String `tfsdk:"image_id"` - Name types.String `tfsdk:"name"` - DiskFormat types.String `tfsdk:"disk_format"` - MinDiskSize types.Int64 `tfsdk:"min_disk_size"` - MinRAM types.Int64 `tfsdk:"min_ram"` - Protected types.Bool `tfsdk:"protected"` - Scope types.String `tfsdk:"scope"` - Config types.Object `tfsdk:"config"` - Checksum types.Object `tfsdk:"checksum"` - Labels types.Map `tfsdk:"labels"` - LocalFilePath types.String `tfsdk:"local_file_path"` -} - -// Struct corresponding to Model.Config -type configModel struct { - BootMenu types.Bool `tfsdk:"boot_menu"` - CDROMBus types.String `tfsdk:"cdrom_bus"` - DiskBus types.String `tfsdk:"disk_bus"` - NICModel types.String `tfsdk:"nic_model"` - OperatingSystem types.String `tfsdk:"operating_system"` - OperatingSystemDistro types.String `tfsdk:"operating_system_distro"` - OperatingSystemVersion types.String `tfsdk:"operating_system_version"` - RescueBus types.String `tfsdk:"rescue_bus"` - RescueDevice types.String `tfsdk:"rescue_device"` - SecureBoot types.Bool `tfsdk:"secure_boot"` - UEFI types.Bool `tfsdk:"uefi"` - VideoModel types.String `tfsdk:"video_model"` - VirtioScsi types.Bool `tfsdk:"virtio_scsi"` -} - -// Types corresponding to configModel -var configTypes = map[string]attr.Type{ - "boot_menu": basetypes.BoolType{}, - "cdrom_bus": basetypes.StringType{}, - "disk_bus": basetypes.StringType{}, - "nic_model": basetypes.StringType{}, - "operating_system": basetypes.StringType{}, - "operating_system_distro": basetypes.StringType{}, - "operating_system_version": basetypes.StringType{}, - "rescue_bus": basetypes.StringType{}, - "rescue_device": basetypes.StringType{}, - "secure_boot": basetypes.BoolType{}, - "uefi": basetypes.BoolType{}, - "video_model": basetypes.StringType{}, - "virtio_scsi": basetypes.BoolType{}, -} - -// Struct corresponding to Model.Checksum -type checksumModel struct { - Algorithm types.String `tfsdk:"algorithm"` - Digest types.String `tfsdk:"digest"` -} - -// Types corresponding to checksumModel -var checksumTypes = map[string]attr.Type{ - "algorithm": basetypes.StringType{}, - "digest": basetypes.StringType{}, -} - -// NewImageResource is a helper function to simplify the provider implementation. -func NewImageResource() resource.Resource { - return &imageResource{} -} - -// imageResource is the resource implementation. -type imageResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *imageResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_image" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *imageResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *imageResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Image resource schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the image is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "image_id": schema.StringAttribute{ - Description: "The image ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the image.", - Required: true, - }, - "disk_format": schema.StringAttribute{ - Description: "The disk format of the image.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "local_file_path": schema.StringAttribute{ - Description: "The filepath of the raw image file to be uploaded.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - // Validating that the file exists in the plan is useful to avoid - // creating an image resource where the local image upload will fail - validate.FileExists(), - }, - }, - "min_disk_size": schema.Int64Attribute{ - Description: "The minimum disk size of the image in GB.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "min_ram": schema.Int64Attribute{ - Description: "The minimum RAM of the image in MB.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "protected": schema.BoolAttribute{ - Description: "Whether the image is protected.", - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "scope": schema.StringAttribute{ - Description: "The scope of the image.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "config": schema.SingleNestedAttribute{ - Description: "Properties to set hardware and scheduling settings for an image.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "boot_menu": schema.BoolAttribute{ - Description: "Enables the BIOS bootmenu.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "cdrom_bus": schema.StringAttribute{ - Description: "Sets CDROM bus controller type.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "disk_bus": schema.StringAttribute{ - Description: "Sets Disk bus controller type.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "nic_model": schema.StringAttribute{ - Description: "Sets virtual network interface model.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "operating_system": schema.StringAttribute{ - Description: "Enables operating system specific optimizations.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "operating_system_distro": schema.StringAttribute{ - Description: "Operating system distribution.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "operating_system_version": schema.StringAttribute{ - Description: "Version of the operating system.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "rescue_bus": schema.StringAttribute{ - Description: "Sets the device bus when the image is used as a rescue image.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "rescue_device": schema.StringAttribute{ - Description: "Sets the device when the image is used as a rescue image.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "secure_boot": schema.BoolAttribute{ - Description: "Enables Secure Boot.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "uefi": schema.BoolAttribute{ - Description: "Enables UEFI boot.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "video_model": schema.StringAttribute{ - Description: "Sets Graphic device model.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "virtio_scsi": schema.BoolAttribute{ - Description: "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - }, - }, - "checksum": schema.SingleNestedAttribute{ - Description: "Representation of an image checksum.", - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "algorithm": schema.StringAttribute{ - Description: "Algorithm for the checksum of the image data.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "digest": schema.StringAttribute{ - Description: "Hexdigest of the checksum of the image data.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - ctx = core.InitProviderContext(ctx) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new image - imageCreateResp, err := r.client.CreateImage(ctx, projectId, region).CreateImagePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - ctx = tflog.SetField(ctx, "image_id", *imageCreateResp.Id) - - // Get the image object, as the creation response does not contain all fields - image, err := r.client.GetImage(ctx, projectId, region, *imageCreateResp.Id).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, image, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to partially populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Upload image - err = uploadImage(ctx, &resp.Diagnostics, model.LocalFilePath.ValueString(), *imageCreateResp.UploadUrl) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Uploading image: %v", err)) - return - } - - // Wait for image to become available - waiter := wait.UploadImageWaitHandler(ctx, r.client, projectId, region, *imageCreateResp.Id) - waiter = waiter.SetTimeout(7 * 24 * time.Hour) // Set timeout to one week, to make the timeout useless - waitResp, err := waiter.WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Waiting for image to become available: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Image created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - imageId := model.ImageId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "image_id", imageId) - - imageResp, err := r.client.GetImage(ctx, projectId, region, imageId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, imageResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Image read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - imageId := model.ImageId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "image_id", imageId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing image - updatedImage, err := r.client.UpdateImage(ctx, projectId, region, imageId).UpdateImagePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, updatedImage, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Image updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - imageId := model.ImageId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "image_id", imageId) - ctx = tflog.SetField(ctx, "region", region) - - ctx = core.InitProviderContext(ctx) - - // Delete existing image - err := r.client.DeleteImage(ctx, projectId, region, imageId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteImageWaitHandler(ctx, r.client, projectId, region, imageId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("image deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Image deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,region,image_id -func (r *imageResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing image", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[image_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "image_id": idParts[2], - }) - - tflog.Info(ctx, "Image state imported") -} - -func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model, region string) error { - if imageResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var imageId string - if model.ImageId.ValueString() != "" { - imageId = model.ImageId.ValueString() - } else if imageResp.Id != nil { - imageId = *imageResp.Id - } else { - return fmt.Errorf("image id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId) - model.Region = types.StringValue(region) - - // Map config - var configModel = &configModel{} - var configObject basetypes.ObjectValue - diags := diag.Diagnostics{} - if imageResp.Config != nil { - configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu) - configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus()) - configModel.DiskBus = types.StringPointerValue(imageResp.Config.GetDiskBus()) - configModel.NICModel = types.StringPointerValue(imageResp.Config.GetNicModel()) - configModel.OperatingSystem = types.StringPointerValue(imageResp.Config.OperatingSystem) - configModel.OperatingSystemDistro = types.StringPointerValue(imageResp.Config.GetOperatingSystemDistro()) - configModel.OperatingSystemVersion = types.StringPointerValue(imageResp.Config.GetOperatingSystemVersion()) - configModel.RescueBus = types.StringPointerValue(imageResp.Config.GetRescueBus()) - configModel.RescueDevice = types.StringPointerValue(imageResp.Config.GetRescueDevice()) - configModel.SecureBoot = types.BoolPointerValue(imageResp.Config.SecureBoot) - configModel.UEFI = types.BoolPointerValue(imageResp.Config.Uefi) - configModel.VideoModel = types.StringPointerValue(imageResp.Config.GetVideoModel()) - configModel.VirtioScsi = types.BoolPointerValue(iaas.PtrBool(imageResp.Config.GetVirtioScsi())) - - configObject, diags = types.ObjectValue(configTypes, map[string]attr.Value{ - "boot_menu": configModel.BootMenu, - "cdrom_bus": configModel.CDROMBus, - "disk_bus": configModel.DiskBus, - "nic_model": configModel.NICModel, - "operating_system": configModel.OperatingSystem, - "operating_system_distro": configModel.OperatingSystemDistro, - "operating_system_version": configModel.OperatingSystemVersion, - "rescue_bus": configModel.RescueBus, - "rescue_device": configModel.RescueDevice, - "secure_boot": configModel.SecureBoot, - "uefi": configModel.UEFI, - "video_model": configModel.VideoModel, - "virtio_scsi": configModel.VirtioScsi, - }) - } else { - configObject = types.ObjectNull(configTypes) - } - if diags.HasError() { - return fmt.Errorf("creating config: %w", core.DiagsToError(diags)) - } - - // Map checksum - var checksumModel = &checksumModel{} - var checksumObject basetypes.ObjectValue - if imageResp.Checksum != nil { - checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm) - checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest) - checksumObject, diags = types.ObjectValue(checksumTypes, map[string]attr.Value{ - "algorithm": checksumModel.Algorithm, - "digest": checksumModel.Digest, - }) - } else { - checksumObject = types.ObjectNull(checksumTypes) - } - if diags.HasError() { - return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags)) - } - - // Map labels - labels, err := iaasUtils.MapLabels(ctx, imageResp.Labels, model.Labels) - if err != nil { - return err - } - - model.ImageId = types.StringValue(imageId) - model.Name = types.StringPointerValue(imageResp.Name) - model.DiskFormat = types.StringPointerValue(imageResp.DiskFormat) - model.MinDiskSize = types.Int64PointerValue(imageResp.MinDiskSize) - model.MinRAM = types.Int64PointerValue(imageResp.MinRam) - model.Protected = types.BoolPointerValue(imageResp.Protected) - model.Scope = types.StringPointerValue(imageResp.Scope) - model.Labels = labels - model.Config = configObject - model.Checksum = checksumObject - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateImagePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var configModel = &configModel{} - if !(model.Config.IsNull() || model.Config.IsUnknown()) { - diags := model.Config.As(ctx, configModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags)) - } - } - - configPayload := &iaas.ImageConfig{ - BootMenu: conversion.BoolValueToPointer(configModel.BootMenu), - CdromBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.CDROMBus)), - DiskBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.DiskBus)), - NicModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.NICModel)), - OperatingSystem: conversion.StringValueToPointer(configModel.OperatingSystem), - OperatingSystemDistro: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemDistro)), - OperatingSystemVersion: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemVersion)), - RescueBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueBus)), - RescueDevice: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueDevice)), - SecureBoot: conversion.BoolValueToPointer(configModel.SecureBoot), - Uefi: conversion.BoolValueToPointer(configModel.UEFI), - VideoModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.VideoModel)), - VirtioScsi: conversion.BoolValueToPointer(configModel.VirtioScsi), - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.CreateImagePayload{ - Name: conversion.StringValueToPointer(model.Name), - DiskFormat: conversion.StringValueToPointer(model.DiskFormat), - MinDiskSize: conversion.Int64ValueToPointer(model.MinDiskSize), - MinRam: conversion.Int64ValueToPointer(model.MinRAM), - Protected: conversion.BoolValueToPointer(model.Protected), - Config: configPayload, - Labels: &labels, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateImagePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var configModel = &configModel{} - if !(model.Config.IsNull() || model.Config.IsUnknown()) { - diags := model.Config.As(ctx, configModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags)) - } - } - - configPayload := &iaas.ImageConfig{ - BootMenu: conversion.BoolValueToPointer(configModel.BootMenu), - CdromBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.CDROMBus)), - DiskBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.DiskBus)), - NicModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.NICModel)), - OperatingSystem: conversion.StringValueToPointer(configModel.OperatingSystem), - OperatingSystemDistro: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemDistro)), - OperatingSystemVersion: iaas.NewNullableString(conversion.StringValueToPointer(configModel.OperatingSystemVersion)), - RescueBus: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueBus)), - RescueDevice: iaas.NewNullableString(conversion.StringValueToPointer(configModel.RescueDevice)), - SecureBoot: conversion.BoolValueToPointer(configModel.SecureBoot), - Uefi: conversion.BoolValueToPointer(configModel.UEFI), - VideoModel: iaas.NewNullableString(conversion.StringValueToPointer(configModel.VideoModel)), - VirtioScsi: conversion.BoolValueToPointer(configModel.VirtioScsi), - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to go map: %w", err) - } - - // DiskFormat is not sent in the update payload as does not have effect after image upload, - // and the field has RequiresReplace set - return &iaas.UpdateImagePayload{ - Name: conversion.StringValueToPointer(model.Name), - MinDiskSize: conversion.Int64ValueToPointer(model.MinDiskSize), - MinRam: conversion.Int64ValueToPointer(model.MinRAM), - Protected: conversion.BoolValueToPointer(model.Protected), - Config: configPayload, - Labels: &labels, - }, nil -} - -func uploadImage(ctx context.Context, diags *diag.Diagnostics, filePath, uploadURL string) error { - if filePath == "" { - return fmt.Errorf("file path is empty") - } - if uploadURL == "" { - return fmt.Errorf("upload URL is empty") - } - - file, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("open file: %w", err) - } - stat, err := file.Stat() - if err != nil { - return fmt.Errorf("stat file: %w", err) - } - - req, err := http.NewRequest(http.MethodPut, uploadURL, bufio.NewReader(file)) - if err != nil { - return fmt.Errorf("create upload request: %w", err) - } - req.Header.Set("Content-Type", "application/octet-stream") - req.ContentLength = stat.Size() - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("upload image: %w", err) - } - defer func() { - err = resp.Body.Close() - if err != nil { - core.LogAndAddError(ctx, diags, "Error uploading image", fmt.Sprintf("Closing response body: %v", err)) - } - }() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("upload image: %s", resp.Status) - } - - return nil -} diff --git a/stackit/internal/services/iaas/image/resource_test.go b/stackit/internal/services/iaas/image/resource_test.go deleted file mode 100644 index 2040bdd6..00000000 --- a/stackit/internal/services/iaas/image/resource_test.go +++ /dev/null @@ -1,404 +0,0 @@ -package image - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.Image - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - DiskFormat: utils.Ptr("format"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Scope: utils.Ptr("scope"), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("model")), - VirtioScsi: utils.Ptr(true), - }, - Checksum: &iaas.ImageChecksum{ - Algorithm: utils.Ptr("algorithm"), - Digest: utils.Ptr("digest"), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Name: types.StringValue("name"), - DiskFormat: types.StringValue("format"), - MinDiskSize: types.Int64Value(1), - MinRAM: types.Int64Value(1), - Protected: types.BoolValue(true), - Scope: types.StringValue("scope"), - Config: types.ObjectValueMust(configTypes, map[string]attr.Value{ - "boot_menu": types.BoolValue(true), - "cdrom_bus": types.StringValue("cdrom_bus"), - "disk_bus": types.StringValue("disk_bus"), - "nic_model": types.StringValue("model"), - "operating_system": types.StringValue("os"), - "operating_system_distro": types.StringValue("os_distro"), - "operating_system_version": types.StringValue("os_version"), - "rescue_bus": types.StringValue("rescue_bus"), - "rescue_device": types.StringValue("rescue_device"), - "secure_boot": types.BoolValue(true), - "uefi": types.BoolValue(true), - "video_model": types.StringValue("model"), - "virtio_scsi": types.BoolValue(true), - }), - Checksum: types.ObjectValueMust(checksumTypes, map[string]attr.Value{ - "algorithm": types.StringValue("algorithm"), - "digest": types.StringValue("digest"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.Image{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateImagePayload - isValid bool - }{ - { - "ok", - &Model{ - Id: types.StringValue("pid,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Name: types.StringValue("name"), - DiskFormat: types.StringValue("format"), - MinDiskSize: types.Int64Value(1), - MinRAM: types.Int64Value(1), - Protected: types.BoolValue(true), - Config: types.ObjectValueMust(configTypes, map[string]attr.Value{ - "boot_menu": types.BoolValue(true), - "cdrom_bus": types.StringValue("cdrom_bus"), - "disk_bus": types.StringValue("disk_bus"), - "nic_model": types.StringValue("nic_model"), - "operating_system": types.StringValue("os"), - "operating_system_distro": types.StringValue("os_distro"), - "operating_system_version": types.StringValue("os_version"), - "rescue_bus": types.StringValue("rescue_bus"), - "rescue_device": types.StringValue("rescue_device"), - "secure_boot": types.BoolValue(true), - "uefi": types.BoolValue(true), - "video_model": types.StringValue("video_model"), - "virtio_scsi": types.BoolValue(true), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - &iaas.CreateImagePayload{ - Name: utils.Ptr("name"), - DiskFormat: utils.Ptr("format"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("nic_model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("video_model")), - VirtioScsi: utils.Ptr(true), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdateImagePayload - isValid bool - }{ - { - "default_ok", - &Model{ - Id: types.StringValue("pid,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Name: types.StringValue("name"), - DiskFormat: types.StringValue("format"), - MinDiskSize: types.Int64Value(1), - MinRAM: types.Int64Value(1), - Protected: types.BoolValue(true), - Config: types.ObjectValueMust(configTypes, map[string]attr.Value{ - "boot_menu": types.BoolValue(true), - "cdrom_bus": types.StringValue("cdrom_bus"), - "disk_bus": types.StringValue("disk_bus"), - "nic_model": types.StringValue("nic_model"), - "operating_system": types.StringValue("os"), - "operating_system_distro": types.StringValue("os_distro"), - "operating_system_version": types.StringValue("os_version"), - "rescue_bus": types.StringValue("rescue_bus"), - "rescue_device": types.StringValue("rescue_device"), - "secure_boot": types.BoolValue(true), - "uefi": types.BoolValue(true), - "video_model": types.StringValue("video_model"), - "virtio_scsi": types.BoolValue(true), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - &iaas.UpdateImagePayload{ - Name: utils.Ptr("name"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("nic_model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("video_model")), - VirtioScsi: utils.Ptr(true), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func Test_UploadImage(t *testing.T) { - tests := []struct { - name string - filePath string - uploadFails bool - wantErr bool - }{ - { - name: "ok", - filePath: "testdata/mock-image.txt", - uploadFails: false, - wantErr: false, - }, - { - name: "upload_fails", - filePath: "testdata/mock-image.txt", - uploadFails: true, - wantErr: true, - }, - { - name: "file_not_found", - filePath: "testdata/non-existing-file.txt", - uploadFails: false, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup a test server - handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - if tt.uploadFails { - w.WriteHeader(http.StatusInternalServerError) - _, _ = fmt.Fprintln(w, `{"status":"some error occurred"}`) - return - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = fmt.Fprintln(w, `{"status":"ok"}`) - }) - server := httptest.NewServer(handler) - defer server.Close() - uploadURL, err := url.Parse(server.URL) - if err != nil { - t.Error(err) - return - } - - // Call the function - err = uploadImage(context.Background(), &diag.Diagnostics{}, tt.filePath, uploadURL.String()) - if (err != nil) != tt.wantErr { - t.Errorf("uploadImage() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/stackit/internal/services/iaas/image/testdata/mock-image.txt b/stackit/internal/services/iaas/image/testdata/mock-image.txt deleted file mode 100644 index eaa3529c..00000000 --- a/stackit/internal/services/iaas/image/testdata/mock-image.txt +++ /dev/null @@ -1 +0,0 @@ -I am a mock image file \ No newline at end of file diff --git a/stackit/internal/services/iaas/imagev2/datasource.go b/stackit/internal/services/iaas/imagev2/datasource.go deleted file mode 100644 index 01f3b8a2..00000000 --- a/stackit/internal/services/iaas/imagev2/datasource.go +++ /dev/null @@ -1,648 +0,0 @@ -package image - -import ( - "context" - "fmt" - "net/http" - "regexp" - "sort" - - "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &imageDataV2Source{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ImageId types.String `tfsdk:"image_id"` - Name types.String `tfsdk:"name"` - NameRegex types.String `tfsdk:"name_regex"` - SortAscending types.Bool `tfsdk:"sort_ascending"` - Filter types.Object `tfsdk:"filter"` - - DiskFormat types.String `tfsdk:"disk_format"` - MinDiskSize types.Int64 `tfsdk:"min_disk_size"` - MinRAM types.Int64 `tfsdk:"min_ram"` - Protected types.Bool `tfsdk:"protected"` - Scope types.String `tfsdk:"scope"` - Config types.Object `tfsdk:"config"` - Checksum types.Object `tfsdk:"checksum"` - Labels types.Map `tfsdk:"labels"` -} - -type Filter struct { - OS types.String `tfsdk:"os"` - Distro types.String `tfsdk:"distro"` - Version types.String `tfsdk:"version"` - UEFI types.Bool `tfsdk:"uefi"` - SecureBoot types.Bool `tfsdk:"secure_boot"` -} - -// Struct corresponding to Model.Config -type configModel struct { - BootMenu types.Bool `tfsdk:"boot_menu"` - CDROMBus types.String `tfsdk:"cdrom_bus"` - DiskBus types.String `tfsdk:"disk_bus"` - NICModel types.String `tfsdk:"nic_model"` - OperatingSystem types.String `tfsdk:"operating_system"` - OperatingSystemDistro types.String `tfsdk:"operating_system_distro"` - OperatingSystemVersion types.String `tfsdk:"operating_system_version"` - RescueBus types.String `tfsdk:"rescue_bus"` - RescueDevice types.String `tfsdk:"rescue_device"` - SecureBoot types.Bool `tfsdk:"secure_boot"` - UEFI types.Bool `tfsdk:"uefi"` - VideoModel types.String `tfsdk:"video_model"` - VirtioScsi types.Bool `tfsdk:"virtio_scsi"` -} - -// Types corresponding to configModel -var configTypes = map[string]attr.Type{ - "boot_menu": basetypes.BoolType{}, - "cdrom_bus": basetypes.StringType{}, - "disk_bus": basetypes.StringType{}, - "nic_model": basetypes.StringType{}, - "operating_system": basetypes.StringType{}, - "operating_system_distro": basetypes.StringType{}, - "operating_system_version": basetypes.StringType{}, - "rescue_bus": basetypes.StringType{}, - "rescue_device": basetypes.StringType{}, - "secure_boot": basetypes.BoolType{}, - "uefi": basetypes.BoolType{}, - "video_model": basetypes.StringType{}, - "virtio_scsi": basetypes.BoolType{}, -} - -// Struct corresponding to Model.Checksum -type checksumModel struct { - Algorithm types.String `tfsdk:"algorithm"` - Digest types.String `tfsdk:"digest"` -} - -// Types corresponding to checksumModel -var checksumTypes = map[string]attr.Type{ - "algorithm": basetypes.StringType{}, - "digest": basetypes.StringType{}, -} - -// NewImageV2DataSource is a helper function to simplify the provider implementation. -func NewImageV2DataSource() datasource.DataSource { - return &imageDataV2Source{} -} - -// imageDataV2Source is the data source implementation. -type imageDataV2Source struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *imageDataV2Source) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_image_v2" -} - -func (d *imageDataV2Source) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_image_v2", "datasource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -func (d *imageDataV2Source) ConfigValidators(_ context.Context) []datasource.ConfigValidator { - return []datasource.ConfigValidator{ - datasourcevalidator.Conflicting( - path.MatchRoot("name"), - path.MatchRoot("name_regex"), - path.MatchRoot("image_id"), - ), - datasourcevalidator.AtLeastOneOf( - path.MatchRoot("name"), - path.MatchRoot("name_regex"), - path.MatchRoot("image_id"), - path.MatchRoot("filter"), - ), - } -} - -// Schema defines the schema for the datasource. -func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := features.AddBetaDescription(fmt.Sprintf( - "%s\n\n~> %s", - "Image datasource schema. Must have a `region` specified in the provider configuration.", - "Important: When using the `name`, `name_regex`, or `filter` attributes to select images dynamically, be aware that image IDs may change frequently. Each OS patch or update results in a new unique image ID. If this data source is used to populate fields like `boot_volume.source_id` in a server resource, it may cause Terraform to detect changes and recreate the associated resource.\n\n"+ - "To avoid unintended updates or resource replacements:\n"+ - " - Prefer using a static `image_id` to pin a specific image version.\n"+ - " - If you accept automatic image updates but wish to suppress resource changes, use a `lifecycle` block to ignore relevant changes. For example:\n\n"+ - "```hcl\n"+ - "resource \"stackit_server\" \"example\" {\n"+ - " boot_volume = {\n"+ - " size = 64\n"+ - " source_type = \"image\"\n"+ - " source_id = data.stackit_image.latest.id\n"+ - " }\n"+ - "\n"+ - " lifecycle {\n"+ - " ignore_changes = [boot_volume[0].source_id]\n"+ - " }\n"+ - "}\n"+ - "```\n\n"+ - "You can also list available images using the [STACKIT CLI](https://github.com/stackitcloud/stackit-cli):\n\n"+ - "```bash\n"+ - "stackit image list\n"+ - "```", - ), core.Datasource) - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the image is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "image_id": schema.StringAttribute{ - Description: "Image ID to fetch directly", - Optional: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "Exact image name to match. Optionally applies a `filter` block to further refine results in case multiple images share the same name. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name_regex`.", - Optional: true, - }, - "name_regex": schema.StringAttribute{ - Description: "Regular expression to match against image names. Optionally applies a `filter` block to narrow down results when multiple image names match the regex. The first match is returned, optionally sorted by name in ascending order. Cannot be used together with `name`.", - Optional: true, - }, - "sort_ascending": schema.BoolAttribute{ - Description: "If set to `true`, images are sorted in ascending lexicographical order by image name (such as `Ubuntu 18.04`, `Ubuntu 20.04`, `Ubuntu 22.04`) before selecting the first match. Defaults to `false` (descending such as `Ubuntu 22.04`, `Ubuntu 20.04`, `Ubuntu 18.04`).", - Optional: true, - }, - "filter": schema.SingleNestedAttribute{ - Optional: true, - Description: "Additional filtering options based on image properties. Can be used independently or in conjunction with `name` or `name_regex`.", - Attributes: map[string]schema.Attribute{ - "os": schema.StringAttribute{ - Optional: true, - Description: "Filter images by operating system type, such as `linux` or `windows`.", - }, - "distro": schema.StringAttribute{ - Optional: true, - Description: "Filter images by operating system distribution. For example: `ubuntu`, `ubuntu-arm64`, `debian`, `rhel`, etc.", - }, - "version": schema.StringAttribute{ - Optional: true, - Description: "Filter images by OS distribution version, such as `22.04`, `11`, or `9.1`.", - }, - "uefi": schema.BoolAttribute{ - Optional: true, - Description: "Filter images based on UEFI support. Set to `true` to match images that support UEFI.", - }, - "secure_boot": schema.BoolAttribute{ - Optional: true, - Description: "Filter images with Secure Boot support. Set to `true` to match images that support Secure Boot.", - }, - }, - }, - "disk_format": schema.StringAttribute{ - Description: "The disk format of the image.", - Computed: true, - }, - "min_disk_size": schema.Int64Attribute{ - Description: "The minimum disk size of the image in GB.", - Computed: true, - }, - "min_ram": schema.Int64Attribute{ - Description: "The minimum RAM of the image in MB.", - Computed: true, - }, - "protected": schema.BoolAttribute{ - Description: "Whether the image is protected.", - Computed: true, - }, - "scope": schema.StringAttribute{ - Description: "The scope of the image.", - Computed: true, - }, - "config": schema.SingleNestedAttribute{ - Description: "Properties to set hardware and scheduling settings for an image.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "boot_menu": schema.BoolAttribute{ - Description: "Enables the BIOS bootmenu.", - Computed: true, - }, - "cdrom_bus": schema.StringAttribute{ - Description: "Sets CDROM bus controller type.", - Computed: true, - }, - "disk_bus": schema.StringAttribute{ - Description: "Sets Disk bus controller type.", - Computed: true, - }, - "nic_model": schema.StringAttribute{ - Description: "Sets virtual network interface model.", - Computed: true, - }, - "operating_system": schema.StringAttribute{ - Description: "Enables operating system specific optimizations.", - Computed: true, - }, - "operating_system_distro": schema.StringAttribute{ - Description: "Operating system distribution.", - Computed: true, - }, - "operating_system_version": schema.StringAttribute{ - Description: "Version of the operating system.", - Computed: true, - }, - "rescue_bus": schema.StringAttribute{ - Description: "Sets the device bus when the image is used as a rescue image.", - Computed: true, - }, - "rescue_device": schema.StringAttribute{ - Description: "Sets the device when the image is used as a rescue image.", - Computed: true, - }, - "secure_boot": schema.BoolAttribute{ - Description: "Enables Secure Boot.", - Computed: true, - }, - "uefi": schema.BoolAttribute{ - Description: "Enables UEFI boot.", - Computed: true, - }, - "video_model": schema.StringAttribute{ - Description: "Sets Graphic device model.", - Computed: true, - }, - "virtio_scsi": schema.BoolAttribute{ - Description: "Enables the use of VirtIO SCSI to provide block device access. By default instances use VirtIO Block.", - Computed: true, - }, - }, - }, - "checksum": schema.SingleNestedAttribute{ - Description: "Representation of an image checksum.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "algorithm": schema.StringAttribute{ - Description: "Algorithm for the checksum of the image data.", - Computed: true, - }, - "digest": schema.StringAttribute{ - Description: "Hexdigest of the checksum of the image data.", - Computed: true, - }, - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectID := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - imageID := model.ImageId.ValueString() - name := model.Name.ValueString() - nameRegex := model.NameRegex.ValueString() - sortAscending := model.SortAscending.ValueBool() - - var filter Filter - if !model.Filter.IsNull() && !model.Filter.IsUnknown() { - if diagnostics := model.Filter.As(ctx, &filter, basetypes.ObjectAsOptions{}); diagnostics.HasError() { - resp.Diagnostics.Append(diagnostics...) - return - } - } - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectID) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "image_id", imageID) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "name_regex", nameRegex) - ctx = tflog.SetField(ctx, "sort_ascending", sortAscending) - - var imageResp *iaas.Image - var err error - - // Case 1: Direct lookup by image ID - if imageID != "" { - imageResp, err = d.client.GetImage(ctx, projectID, region, imageID).Execute() - if err != nil { - utils.LogError(ctx, &resp.Diagnostics, err, "Reading image", - fmt.Sprintf("Image with ID %q does not exist in project %q.", imageID, projectID), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectID), - }) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - } else { - // Case 2: Lookup by name or name_regex - - // Compile regex - var compiledRegex *regexp.Regexp - if nameRegex != "" { - compiledRegex, err = regexp.Compile(nameRegex) - if err != nil { - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Invalid name_regex", err.Error()) - return - } - } - - // Fetch all available images - imageList, err := d.client.ListImages(ctx, projectID, region).Execute() - if err != nil { - utils.LogError(ctx, &resp.Diagnostics, err, "List images", "Unable to fetch images", nil) - return - } - - ctx = core.LogResponse(ctx) - - // Step 1: Match images by name or regular expression (name or name_regex, if provided) - var matchedImages []*iaas.Image - for i := range *imageList.Items { - img := &(*imageList.Items)[i] - if name != "" && img.Name != nil && *img.Name == name { - matchedImages = append(matchedImages, img) - } - if compiledRegex != nil && img.Name != nil && compiledRegex.MatchString(*img.Name) { - matchedImages = append(matchedImages, img) - } - // If neither name nor name_regex is specified, include all images for filter evaluation later - if name == "" && nameRegex == "" { - matchedImages = append(matchedImages, img) - } - } - - // Step 2: Sort matched images by name (optional, based on sortAscending flag) - if len(matchedImages) > 1 { - sortImagesByName(matchedImages, sortAscending) - } - - // Step 3: Apply additional filtering based on OS, distro, version, UEFI, secure boot, etc. - var filteredImages []*iaas.Image - for _, img := range matchedImages { - if imageMatchesFilter(img, &filter) { - filteredImages = append(filteredImages, img) - } - } - - // Check if any images passed all filters; warn if no matching image was found - if len(filteredImages) == 0 { - core.LogAndAddWarning(ctx, &resp.Diagnostics, "No match", - "No matching image found using name, name_regex, and filter criteria.") - return - } - - // Step 4: Use the first image from the filtered and sorted result list - imageResp = filteredImages[0] - } - - err = mapDataSourceFields(ctx, imageResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "image read") -} - -func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *DataSourceModel, region string) error { - if imageResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var imageId string - if model.ImageId.ValueString() != "" { - imageId = model.ImageId.ValueString() - } else if imageResp.Id != nil { - imageId = *imageResp.Id - } else { - return fmt.Errorf("image id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, imageId) - model.Region = types.StringValue(region) - - // Map config - var configModel = &configModel{} - var configObject basetypes.ObjectValue - diags := diag.Diagnostics{} - if imageResp.Config != nil { - configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu) - configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus()) - configModel.DiskBus = types.StringPointerValue(imageResp.Config.GetDiskBus()) - configModel.NICModel = types.StringPointerValue(imageResp.Config.GetNicModel()) - configModel.OperatingSystem = types.StringPointerValue(imageResp.Config.OperatingSystem) - configModel.OperatingSystemDistro = types.StringPointerValue(imageResp.Config.GetOperatingSystemDistro()) - configModel.OperatingSystemVersion = types.StringPointerValue(imageResp.Config.GetOperatingSystemVersion()) - configModel.RescueBus = types.StringPointerValue(imageResp.Config.GetRescueBus()) - configModel.RescueDevice = types.StringPointerValue(imageResp.Config.GetRescueDevice()) - configModel.SecureBoot = types.BoolPointerValue(imageResp.Config.SecureBoot) - configModel.UEFI = types.BoolPointerValue(imageResp.Config.Uefi) - configModel.VideoModel = types.StringPointerValue(imageResp.Config.GetVideoModel()) - configModel.VirtioScsi = types.BoolPointerValue(iaas.PtrBool(imageResp.Config.GetVirtioScsi())) - - configObject, diags = types.ObjectValue(configTypes, map[string]attr.Value{ - "boot_menu": configModel.BootMenu, - "cdrom_bus": configModel.CDROMBus, - "disk_bus": configModel.DiskBus, - "nic_model": configModel.NICModel, - "operating_system": configModel.OperatingSystem, - "operating_system_distro": configModel.OperatingSystemDistro, - "operating_system_version": configModel.OperatingSystemVersion, - "rescue_bus": configModel.RescueBus, - "rescue_device": configModel.RescueDevice, - "secure_boot": configModel.SecureBoot, - "uefi": configModel.UEFI, - "video_model": configModel.VideoModel, - "virtio_scsi": configModel.VirtioScsi, - }) - } else { - configObject = types.ObjectNull(configTypes) - } - if diags.HasError() { - return fmt.Errorf("creating config: %w", core.DiagsToError(diags)) - } - - // Map checksum - var checksumModel = &checksumModel{} - var checksumObject basetypes.ObjectValue - if imageResp.Checksum != nil { - checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm) - checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest) - checksumObject, diags = types.ObjectValue(checksumTypes, map[string]attr.Value{ - "algorithm": checksumModel.Algorithm, - "digest": checksumModel.Digest, - }) - } else { - checksumObject = types.ObjectNull(checksumTypes) - } - if diags.HasError() { - return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags)) - } - - // Map labels - labels, err := iaasUtils.MapLabels(ctx, imageResp.Labels, model.Labels) - if err != nil { - return err - } - - model.ImageId = types.StringValue(imageId) - model.Name = types.StringPointerValue(imageResp.Name) - model.DiskFormat = types.StringPointerValue(imageResp.DiskFormat) - model.MinDiskSize = types.Int64PointerValue(imageResp.MinDiskSize) - model.MinRAM = types.Int64PointerValue(imageResp.MinRam) - model.Protected = types.BoolPointerValue(imageResp.Protected) - model.Scope = types.StringPointerValue(imageResp.Scope) - model.Labels = labels - model.Config = configObject - model.Checksum = checksumObject - return nil -} - -// imageMatchesFilter checks whether a given image matches all specified filter conditions. -// It returns true only if all non-null fields in the filter match corresponding fields in the image's config. -func imageMatchesFilter(img *iaas.Image, filter *Filter) bool { - if filter == nil { - return true - } - - if img.Config == nil { - return false - } - - cfg := img.Config - - if !filter.OS.IsNull() && - (cfg.OperatingSystem == nil || filter.OS.ValueString() != *cfg.OperatingSystem) { - return false - } - - if !filter.Distro.IsNull() && - (cfg.OperatingSystemDistro == nil || cfg.OperatingSystemDistro.Get() == nil || - filter.Distro.ValueString() != *cfg.OperatingSystemDistro.Get()) { - return false - } - - if !filter.Version.IsNull() && - (cfg.OperatingSystemVersion == nil || cfg.OperatingSystemVersion.Get() == nil || - filter.Version.ValueString() != *cfg.OperatingSystemVersion.Get()) { - return false - } - - if !filter.UEFI.IsNull() && - (cfg.Uefi == nil || filter.UEFI.ValueBool() != *cfg.Uefi) { - return false - } - - if !filter.SecureBoot.IsNull() && - (cfg.SecureBoot == nil || filter.SecureBoot.ValueBool() != *cfg.SecureBoot) { - return false - } - - return true -} - -// sortImagesByName sorts a slice of images by name, respecting nils and order direction. -func sortImagesByName(images []*iaas.Image, sortAscending bool) { - if len(images) <= 1 { - return - } - - sort.SliceStable(images, func(i, j int) bool { - a, b := images[i].Name, images[j].Name - - switch { - case a == nil && b == nil: - return false // Equal - case a == nil: - return false // Nil goes after non-nil - case b == nil: - return true // Non-nil goes before nil - case sortAscending: - return *a < *b - default: - return *a > *b - } - }) -} diff --git a/stackit/internal/services/iaas/imagev2/datasource_test.go b/stackit/internal/services/iaas/imagev2/datasource_test.go deleted file mode 100644 index 3d27ed4f..00000000 --- a/stackit/internal/services/iaas/imagev2/datasource_test.go +++ /dev/null @@ -1,484 +0,0 @@ -package image - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapDataSourceFields(t *testing.T) { - type args struct { - state DataSourceModel - input *iaas.Image - region string - } - tests := []struct { - description string - args args - expected DataSourceModel - isValid bool - }{ - { - description: "default_values", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu01,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - DiskFormat: utils.Ptr("format"), - MinDiskSize: utils.Ptr(int64(1)), - MinRam: utils.Ptr(int64(1)), - Protected: utils.Ptr(true), - Scope: utils.Ptr("scope"), - Config: &iaas.ImageConfig{ - BootMenu: utils.Ptr(true), - CdromBus: iaas.NewNullableString(utils.Ptr("cdrom_bus")), - DiskBus: iaas.NewNullableString(utils.Ptr("disk_bus")), - NicModel: iaas.NewNullableString(utils.Ptr("model")), - OperatingSystem: utils.Ptr("os"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("os_distro")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("os_version")), - RescueBus: iaas.NewNullableString(utils.Ptr("rescue_bus")), - RescueDevice: iaas.NewNullableString(utils.Ptr("rescue_device")), - SecureBoot: utils.Ptr(true), - Uefi: utils.Ptr(true), - VideoModel: iaas.NewNullableString(utils.Ptr("model")), - VirtioScsi: utils.Ptr(true), - }, - Checksum: &iaas.ImageChecksum{ - Algorithm: utils.Ptr("algorithm"), - Digest: utils.Ptr("digest"), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - region: "eu02", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu02,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Name: types.StringValue("name"), - DiskFormat: types.StringValue("format"), - MinDiskSize: types.Int64Value(1), - MinRAM: types.Int64Value(1), - Protected: types.BoolValue(true), - Scope: types.StringValue("scope"), - Config: types.ObjectValueMust(configTypes, map[string]attr.Value{ - "boot_menu": types.BoolValue(true), - "cdrom_bus": types.StringValue("cdrom_bus"), - "disk_bus": types.StringValue("disk_bus"), - "nic_model": types.StringValue("model"), - "operating_system": types.StringValue("os"), - "operating_system_distro": types.StringValue("os_distro"), - "operating_system_version": types.StringValue("os_version"), - "rescue_bus": types.StringValue("rescue_bus"), - "rescue_device": types.StringValue("rescue_device"), - "secure_boot": types.BoolValue(true), - "uefi": types.BoolValue(true), - "video_model": types.StringValue("model"), - "virtio_scsi": types.BoolValue(true), - }), - Checksum: types.ObjectValueMust(checksumTypes, map[string]attr.Value{ - "algorithm": types.StringValue("algorithm"), - "digest": types.StringValue("digest"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - input: &iaas.Image{ - Id: utils.Ptr("iid"), - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu01,iid"), - ProjectId: types.StringValue("pid"), - ImageId: types.StringValue("iid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.Image{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestImageMatchesFilter(t *testing.T) { - testCases := []struct { - name string - img *iaas.Image - filter *Filter - expected bool - }{ - { - name: "nil filter - always match", - img: &iaas.Image{Config: &iaas.ImageConfig{}}, - filter: nil, - expected: true, - }, - { - name: "nil config - always false", - img: &iaas.Image{Config: nil}, - filter: &Filter{}, - expected: false, - }, - { - name: "all fields match", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystem: utils.Ptr("linux"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("ubuntu")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("22.04")), - Uefi: utils.Ptr(true), - SecureBoot: utils.Ptr(true), - }, - }, - filter: &Filter{ - OS: types.StringValue("linux"), - Distro: types.StringValue("ubuntu"), - Version: types.StringValue("22.04"), - UEFI: types.BoolValue(true), - SecureBoot: types.BoolValue(true), - }, - expected: true, - }, - { - name: "OS mismatch", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystem: utils.Ptr("windows"), - }, - }, - filter: &Filter{ - OS: types.StringValue("linux"), - }, - expected: false, - }, - { - name: "Distro mismatch", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("debian")), - }, - }, - filter: &Filter{ - Distro: types.StringValue("ubuntu"), - }, - expected: false, - }, - { - name: "Version mismatch", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("20.04")), - }, - }, - filter: &Filter{ - Version: types.StringValue("22.04"), - }, - expected: false, - }, - { - name: "UEFI mismatch", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - Uefi: utils.Ptr(false), - }, - }, - filter: &Filter{ - UEFI: types.BoolValue(true), - }, - expected: false, - }, - { - name: "SecureBoot mismatch", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - SecureBoot: utils.Ptr(false), - }, - }, - filter: &Filter{ - SecureBoot: types.BoolValue(true), - }, - expected: false, - }, - { - name: "SecureBoot match - true", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - SecureBoot: utils.Ptr(true), - }, - }, - filter: &Filter{ - SecureBoot: types.BoolValue(true), - }, - expected: true, - }, - { - name: "SecureBoot match - false", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - SecureBoot: utils.Ptr(false), - }, - }, - filter: &Filter{ - SecureBoot: types.BoolValue(false), - }, - expected: true, - }, - { - name: "SecureBoot field missing in image but required in filter", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - SecureBoot: nil, - }, - }, - filter: &Filter{ - SecureBoot: types.BoolValue(true), - }, - expected: false, - }, - { - name: "partial filter match - only distro set and match", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("ubuntu")), - }, - }, - filter: &Filter{ - Distro: types.StringValue("ubuntu"), - }, - expected: true, - }, - { - name: "partial filter match - distro mismatch", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("centos")), - }, - }, - filter: &Filter{ - Distro: types.StringValue("ubuntu"), - }, - expected: false, - }, - { - name: "filter provided but attribute is null in image", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystemDistro: nil, - }, - }, - filter: &Filter{ - Distro: types.StringValue("ubuntu"), - }, - expected: false, - }, - { - name: "image has valid config, but filter has null values", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystem: utils.Ptr("linux"), - OperatingSystemDistro: iaas.NewNullableString(utils.Ptr("ubuntu")), - OperatingSystemVersion: iaas.NewNullableString(utils.Ptr("22.04")), - Uefi: utils.Ptr(false), - SecureBoot: utils.Ptr(false), - }, - }, - filter: &Filter{ - OS: types.StringNull(), - Distro: types.StringNull(), - Version: types.StringNull(), - UEFI: types.BoolNull(), - SecureBoot: types.BoolNull(), - }, - expected: true, - }, - { - name: "image has nil fields in config, filter expects values", - img: &iaas.Image{ - Config: &iaas.ImageConfig{ - OperatingSystem: nil, - OperatingSystemDistro: nil, - OperatingSystemVersion: nil, - Uefi: nil, - SecureBoot: nil, - }, - }, - filter: &Filter{ - OS: types.StringValue("linux"), - Distro: types.StringValue("ubuntu"), - Version: types.StringValue("22.04"), - UEFI: types.BoolValue(true), - SecureBoot: types.BoolValue(true), - }, - expected: false, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := imageMatchesFilter(tc.img, tc.filter) - if result != tc.expected { - t.Errorf("Expected match = %v, got %v", tc.expected, result) - } - }) - } -} - -func TestSortImagesByName(t *testing.T) { - tests := []struct { - desc string - input []*iaas.Image - ascending bool - wantSorted []string - }{ - { - desc: "ascending by name", - ascending: true, - input: []*iaas.Image{ - {Name: utils.Ptr("Ubuntu 22.04")}, - {Name: utils.Ptr("Ubuntu 18.04")}, - {Name: utils.Ptr("Ubuntu 20.04")}, - }, - wantSorted: []string{"Ubuntu 18.04", "Ubuntu 20.04", "Ubuntu 22.04"}, - }, - { - desc: "descending by name", - ascending: false, - input: []*iaas.Image{ - {Name: utils.Ptr("Ubuntu 22.04")}, - {Name: utils.Ptr("Ubuntu 18.04")}, - {Name: utils.Ptr("Ubuntu 20.04")}, - }, - wantSorted: []string{"Ubuntu 22.04", "Ubuntu 20.04", "Ubuntu 18.04"}, - }, - { - desc: "nil names go last ascending", - ascending: true, - input: []*iaas.Image{ - {Name: nil}, - {Name: utils.Ptr("Ubuntu 18.04")}, - {Name: nil}, - {Name: utils.Ptr("Ubuntu 20.04")}, - }, - wantSorted: []string{"Ubuntu 18.04", "Ubuntu 20.04", "", ""}, - }, - { - desc: "nil names go last descending", - ascending: false, - input: []*iaas.Image{ - {Name: nil}, - {Name: utils.Ptr("Ubuntu 18.04")}, - {Name: utils.Ptr("Ubuntu 20.04")}, - {Name: nil}, - }, - wantSorted: []string{"Ubuntu 20.04", "Ubuntu 18.04", "", ""}, - }, - { - desc: "empty slice", - ascending: true, - input: []*iaas.Image{}, - wantSorted: []string{}, - }, - { - desc: "single element slice", - ascending: true, - input: []*iaas.Image{ - {Name: utils.Ptr("Ubuntu 22.04")}, - }, - wantSorted: []string{"Ubuntu 22.04"}, - }, - } - - for _, tc := range tests { - t.Run(tc.desc, func(t *testing.T) { - sortImagesByName(tc.input, tc.ascending) - - gotNames := make([]string, len(tc.input)) - for i, img := range tc.input { - if img.Name == nil { - gotNames[i] = "" - } else { - gotNames[i] = *img.Name - } - } - - if diff := cmp.Diff(tc.wantSorted, gotNames); diff != "" { - t.Fatalf("incorrect sort order (-want +got):\n%s", diff) - } - }) - } -} diff --git a/stackit/internal/services/iaas/keypair/const.go b/stackit/internal/services/iaas/keypair/const.go deleted file mode 100644 index 230fd956..00000000 --- a/stackit/internal/services/iaas/keypair/const.go +++ /dev/null @@ -1,24 +0,0 @@ -package keypair - -const exampleUsageWithServer = ` - -### Usage with server` + "\n" + - "```terraform" + ` -resource "stackit_key_pair" "keypair" { - name = "example-key-pair" - public_key = chomp(file("path/to/id_rsa.pub")) -} - -resource "stackit_server" "example-server" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-server" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - availability_zone = "eu01-1" - machine_type = "g2i.1" - keypair_name = "example-key-pair" -} -` + "\n```" diff --git a/stackit/internal/services/iaas/keypair/datasource.go b/stackit/internal/services/iaas/keypair/datasource.go deleted file mode 100644 index 455e043c..00000000 --- a/stackit/internal/services/iaas/keypair/datasource.go +++ /dev/null @@ -1,129 +0,0 @@ -package keypair - -import ( - "context" - "fmt" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &keyPairDataSource{} -) - -// NewKeyPairDataSource is a helper function to simplify the provider implementation. -func NewKeyPairDataSource() datasource.DataSource { - return &keyPairDataSource{} -} - -// keyPairDataSource is the data source implementation. -type keyPairDataSource struct { - client *iaas.APIClient -} - -// Metadata returns the data source type name. -func (d *keyPairDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_key_pair" -} - -func (d *keyPairDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (d *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Key pair resource schema. Must have a `region` specified in the provider configuration." - - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It takes the value of the key pair \"`name`\".", - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "The name of the SSH key pair.", - Required: true, - }, - "public_key": schema.StringAttribute{ - Description: "A string representation of the public SSH key. E.g., `ssh-rsa ` or `ssh-ed25519 `.", - Computed: true, - }, - "fingerprint": schema.StringAttribute{ - Description: "The fingerprint of the public SSH key.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container.", - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - name := model.Name.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "name", name) - - keypairResp, err := d.client.GetKeyPair(ctx, name).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading key pair", - fmt.Sprintf("Key pair with name %q does not exist.", name), - nil, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, keypairResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key pair read") -} diff --git a/stackit/internal/services/iaas/keypair/resource.go b/stackit/internal/services/iaas/keypair/resource.go deleted file mode 100644 index 4c709b33..00000000 --- a/stackit/internal/services/iaas/keypair/resource.go +++ /dev/null @@ -1,387 +0,0 @@ -package keypair - -import ( - "context" - "fmt" - "net/http" - "strings" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &keyPairResource{} - _ resource.ResourceWithConfigure = &keyPairResource{} - _ resource.ResourceWithImportState = &keyPairResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - Name types.String `tfsdk:"name"` - PublicKey types.String `tfsdk:"public_key"` - Fingerprint types.String `tfsdk:"fingerprint"` - Labels types.Map `tfsdk:"labels"` -} - -// NewKeyPairResource is a helper function to simplify the provider implementation. -func NewKeyPairResource() resource.Resource { - return &keyPairResource{} -} - -// keyPairResource is the resource implementation. -type keyPairResource struct { - client *iaas.APIClient -} - -// Metadata returns the resource type name. -func (r *keyPairResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_key_pair" -} - -// Configure adds the provider configured client to the resource. -func (r *keyPairResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *keyPairResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Key pair resource schema. Must have a `region` specified in the provider configuration. Allows uploading an SSH public key to be used for server authentication." - - resp.Schema = schema.Schema{ - MarkdownDescription: description + "\n\n" + exampleUsageWithServer, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It takes the value of the key pair \"`name`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the SSH key pair.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "public_key": schema.StringAttribute{ - Description: "A string representation of the public SSH key. E.g., `ssh-rsa ` or `ssh-ed25519 `.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "fingerprint": schema.StringAttribute{ - Description: "The fingerprint of the public SSH key.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container.", - ElementType: types.StringType, - Optional: true, - }, - }, - } -} - -// ModifyPlan will be called in the Plan phase. -// It will check if the plan contains a change that requires replacement. If yes, it will show a warning to the user. -func (r *keyPairResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - // If the state is empty we are creating a new resource - // If the plan is empty we are deleting the resource - // In both cases we don't need to check for replacement - if req.Plan.Raw.IsNull() || req.State.Raw.IsNull() { - return - } - - var planModel Model - diags := req.Plan.Get(ctx, &planModel) - resp.Diagnostics.Append(diags...) - - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - - if planModel.PublicKey.ValueString() != stateModel.PublicKey.ValueString() { - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Key pair public key change", "Changing the public key will trigger a replacement of the key pair resource. The new key pair will not be valid to access servers on which the old key was used, as the key is only registered during server creation.") - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *keyPairResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - name := model.Name.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "name", name) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key pair", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new key pair - - keyPair, err := r.client.CreateKeyPair(ctx).CreateKeyPairPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key pair", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, keyPair, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key pair", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key pair created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *keyPairResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - name := model.Name.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "name", name) - - keyPairResp, err := r.client.GetKeyPair(ctx, name).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, keyPairResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key pair read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *keyPairResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - name := model.Name.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "name", name) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key pair", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing key pair - updatedKeyPair, err := r.client.UpdateKeyPair(ctx, name).UpdateKeyPairPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key pair", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, updatedKeyPair, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key pair", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "key pair updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *keyPairResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - name := model.Name.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "name", name) - - // Delete existing key pair - err := r.client.DeleteKeyPair(ctx, name).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting key pair", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Key pair deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,key_pair_id -func (r *keyPairResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 1 || idParts[0] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing key pair", - fmt.Sprintf("Expected import identifier with format: [name] Got: %q", req.ID), - ) - return - } - - name := idParts[0] - ctx = tflog.SetField(ctx, "name", name) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...) - tflog.Info(ctx, "Key pair state imported") -} - -func mapFields(ctx context.Context, keyPairResp *iaas.Keypair, model *Model) error { - if keyPairResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var name string - if model.Name.ValueString() != "" { - name = model.Name.ValueString() - } else if keyPairResp.Name != nil { - name = *keyPairResp.Name - } else { - return fmt.Errorf("key pair name not present") - } - - model.Id = types.StringValue(name) - model.PublicKey = types.StringPointerValue(keyPairResp.PublicKey) - model.Fingerprint = types.StringPointerValue(keyPairResp.Fingerprint) - - var err error - model.Labels, err = iaasUtils.MapLabels(ctx, keyPairResp.Labels, model.Labels) - if err != nil { - return err - } - - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateKeyPairPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.CreateKeyPairPayload{ - Name: conversion.StringValueToPointer(model.Name), - PublicKey: conversion.StringValueToPointer(model.PublicKey), - Labels: &labels, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateKeyPairPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.UpdateKeyPairPayload{ - Labels: &labels, - }, nil -} diff --git a/stackit/internal/services/iaas/keypair/resource_test.go b/stackit/internal/services/iaas/keypair/resource_test.go deleted file mode 100644 index ed3af09a..00000000 --- a/stackit/internal/services/iaas/keypair/resource_test.go +++ /dev/null @@ -1,211 +0,0 @@ -package keypair - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *iaas.Keypair - expected Model - isValid bool - }{ - { - "default_values", - Model{ - Name: types.StringValue("name"), - }, - &iaas.Keypair{ - Name: utils.Ptr("name"), - }, - Model{ - Id: types.StringValue("name"), - Name: types.StringValue("name"), - PublicKey: types.StringNull(), - Fingerprint: types.StringNull(), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "simple_values", - Model{ - Name: types.StringValue("name"), - }, - &iaas.Keypair{ - Name: utils.Ptr("name"), - PublicKey: utils.Ptr("public_key"), - Fingerprint: utils.Ptr("fingerprint"), - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - Model{ - Id: types.StringValue("name"), - Name: types.StringValue("name"), - PublicKey: types.StringValue("public_key"), - Fingerprint: types.StringValue("fingerprint"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - true, - }, - { - "empty_labels", - Model{ - Name: types.StringValue("name"), - }, - &iaas.Keypair{ - Name: utils.Ptr("name"), - PublicKey: utils.Ptr("public_key"), - Fingerprint: utils.Ptr("fingerprint"), - Labels: &map[string]interface{}{}, - }, - Model{ - Id: types.StringValue("name"), - Name: types.StringValue("name"), - PublicKey: types.StringValue("public_key"), - Fingerprint: types.StringValue("fingerprint"), - Labels: types.MapNull(types.StringType), - }, - true, - }, - { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{}, - &iaas.Keypair{ - PublicKey: utils.Ptr("public_key"), - Fingerprint: utils.Ptr("fingerprint"), - Labels: &map[string]interface{}{}, - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateKeyPairPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - PublicKey: types.StringValue("public_key"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key1": types.StringValue("value1"), - "key2": types.StringValue("value2"), - }), - }, - &iaas.CreateKeyPairPayload{ - Name: utils.Ptr("name"), - PublicKey: utils.Ptr("public_key"), - Labels: &map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdateKeyPairPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - PublicKey: types.StringValue("public_key"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key1": types.StringValue("value1"), - "key2": types.StringValue("value2"), - }), - }, - &iaas.UpdateKeyPairPayload{ - Labels: &map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/machinetype/datasource.go b/stackit/internal/services/iaas/machinetype/datasource.go deleted file mode 100644 index c4d89e91..00000000 --- a/stackit/internal/services/iaas/machinetype/datasource.go +++ /dev/null @@ -1,263 +0,0 @@ -package machineType - -import ( - "context" - "fmt" - "net/http" - "sort" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -// Ensure the implementation satisfies the expected interfaces. -var _ datasource.DataSource = &machineTypeDataSource{} - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // required by Terraform to identify state - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - SortAscending types.Bool `tfsdk:"sort_ascending"` - Filter types.String `tfsdk:"filter"` - Description types.String `tfsdk:"description"` - Disk types.Int64 `tfsdk:"disk"` - ExtraSpecs types.Map `tfsdk:"extra_specs"` - Name types.String `tfsdk:"name"` - Ram types.Int64 `tfsdk:"ram"` - Vcpus types.Int64 `tfsdk:"vcpus"` -} - -// NewMachineTypeDataSource instantiates the data source -func NewMachineTypeDataSource() datasource.DataSource { - return &machineTypeDataSource{} -} - -type machineTypeDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_machine_type" -} - -func (d *machineTypeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &d.providerData, &resp.Diagnostics, "stackit_machine_type", "datasource") - if resp.Diagnostics.HasError() { - return - } - - client := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = client - - tflog.Info(ctx, "IAAS client configured") -} - -func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("Machine type data source.", core.Datasource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`image_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "sort_ascending": schema.BoolAttribute{ - Description: "Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false`", - Optional: true, - }, - "filter": schema.StringAttribute{ - Description: "Expr-lang filter for filtering machine types.\n\n" + - "Examples:\n" + - "- vcpus == 2\n" + - "- ram >= 2048\n" + - "- extraSpecs.cpu == \"intel-icelake-generic\"\n" + - "- extraSpecs.cpu == \"intel-icelake-generic\" && vcpus == 2\n\n" + - "Syntax reference: https://expr-lang.org/docs/language-definition\n\n" + - "You can also list available machine-types using the [STACKIT CLI](https://github.com/stackitcloud/stackit-cli):\n\n" + - "```bash\n" + - "stackit server machine-type list\n" + - "```", - Required: true, - }, - "description": schema.StringAttribute{ - Description: "Machine type description.", - Computed: true, - }, - "disk": schema.Int64Attribute{ - Description: "Disk size in GB.", - Computed: true, - }, - "extra_specs": schema.MapAttribute{ - Description: "Extra specs (e.g., CPU type, overcommit ratio).", - ElementType: types.StringType, - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "Name of the machine type (e.g. 's1.2').", - Computed: true, - }, - "ram": schema.Int64Attribute{ - Description: "RAM size in MB.", - Computed: true, - }, - "vcpus": schema.Int64Attribute{ - Description: "Number of vCPUs.", - Computed: true, - }, - }, - } -} - -func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - sortAscending := model.SortAscending.ValueBool() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "filter_is_null", model.Filter.IsNull()) - ctx = tflog.SetField(ctx, "filter_is_unknown", model.Filter.IsUnknown()) - - listMachineTypeReq := d.client.ListMachineTypes(ctx, projectId, region) - - if !model.Filter.IsNull() && !model.Filter.IsUnknown() && strings.TrimSpace(model.Filter.ValueString()) != "" { - listMachineTypeReq = listMachineTypeReq.Filter(strings.TrimSpace(model.Filter.ValueString())) - } - - apiResp, err := listMachineTypeReq.Execute() - if err != nil { - utils.LogError(ctx, &resp.Diagnostics, err, "Failed to read machine types", - fmt.Sprintf("Unable to retrieve machine types for project %q %s.", projectId, err), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Access denied to project %q.", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - if apiResp.Items == nil || len(*apiResp.Items) == 0 { - core.LogAndAddWarning(ctx, &resp.Diagnostics, "No machine types found", "No matching machine types.") - return - } - - // Convert items to []*iaas.MachineType - machineTypes := make([]*iaas.MachineType, len(*apiResp.Items)) - for i := range *apiResp.Items { - machineTypes[i] = &(*apiResp.Items)[i] - } - - sorted, err := sortMachineTypeByName(machineTypes, sortAscending) - if err != nil { - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Unable to sort", err.Error()) - return - } - - if err := mapDataSourceFields(ctx, sorted[0], &model, region); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading machine type", fmt.Sprintf("Failed to translate API response: %v", err)) - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Successfully read machine type") -} - -func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, model *DataSourceModel, region string) error { - if machineType == nil || model == nil { - return fmt.Errorf("nil input provided") - } - - if machineType.Name == nil || *machineType.Name == "" { - return fmt.Errorf("machine type name is missing") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, *machineType.Name) - model.Region = types.StringValue(region) - model.Name = types.StringPointerValue(machineType.Name) - model.Description = types.StringPointerValue(machineType.Description) - model.Disk = types.Int64PointerValue(machineType.Disk) - model.Ram = types.Int64PointerValue(machineType.Ram) - model.Vcpus = types.Int64PointerValue(machineType.Vcpus) - - extra := types.MapNull(types.StringType) - if machineType.ExtraSpecs != nil && len(*machineType.ExtraSpecs) > 0 { - var diags diag.Diagnostics - extra, diags = types.MapValueFrom(ctx, types.StringType, *machineType.ExtraSpecs) - if diags.HasError() { - return fmt.Errorf("converting extraspecs: %w", core.DiagsToError(diags)) - } - } - model.ExtraSpecs = extra - return nil -} - -func sortMachineTypeByName(input []*iaas.MachineType, ascending bool) ([]*iaas.MachineType, error) { - if input == nil { - return nil, fmt.Errorf("input slice is nil") - } - - // Filter out nil or missing name - var filtered []*iaas.MachineType - for _, m := range input { - if m != nil && m.Name != nil { - filtered = append(filtered, m) - } - } - - sort.SliceStable(filtered, func(i, j int) bool { - if ascending { - return *filtered[i].Name < *filtered[j].Name - } - return *filtered[i].Name > *filtered[j].Name - }) - - return filtered, nil -} diff --git a/stackit/internal/services/iaas/machinetype/datasource_test.go b/stackit/internal/services/iaas/machinetype/datasource_test.go deleted file mode 100644 index 94918810..00000000 --- a/stackit/internal/services/iaas/machinetype/datasource_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package machineType - -import ( - "context" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapDataSourceFields(t *testing.T) { - type args struct { - initial DataSourceModel - input *iaas.MachineType - region string - } - tests := []struct { - name string - args args - expected DataSourceModel - expectError bool - }{ - { - name: "valid simple values", - args: args{ - initial: DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("s1.2"), - Description: utils.Ptr("general-purpose small"), - Disk: utils.Ptr(int64(20)), - Ram: utils.Ptr(int64(2048)), - Vcpus: utils.Ptr(int64(2)), - ExtraSpecs: &map[string]interface{}{ - "cpu": "amd-epycrome-7702", - "overcommit": "1", - "environment": "general", - }, - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu01,s1.2"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("s1.2"), - Description: types.StringValue("general-purpose small"), - Disk: types.Int64Value(20), - Ram: types.Int64Value(2048), - Vcpus: types.Int64Value(2), - ExtraSpecs: types.MapValueMust(types.StringType, map[string]attr.Value{ - "cpu": types.StringValue("amd-epycrome-7702"), - "overcommit": types.StringValue("1"), - "environment": types.StringValue("general"), - }), - Region: types.StringValue("eu01"), - }, - expectError: false, - }, - { - name: "missing name should fail", - args: args{ - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-456"), - }, - input: &iaas.MachineType{ - Description: utils.Ptr("gp-medium"), - }, - }, - expected: DataSourceModel{}, - expectError: true, - }, - { - name: "nil machineType should fail", - args: args{ - initial: DataSourceModel{}, - input: nil, - }, - expected: DataSourceModel{}, - expectError: true, - }, - { - name: "empty extraSpecs should return null map", - args: args{ - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-789"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("m1.noextras"), - Description: utils.Ptr("no extras"), - Disk: utils.Ptr(int64(10)), - Ram: utils.Ptr(int64(1024)), - Vcpus: utils.Ptr(int64(1)), - ExtraSpecs: &map[string]interface{}{}, - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid-789,eu01,m1.noextras"), - ProjectId: types.StringValue("pid-789"), - Name: types.StringValue("m1.noextras"), - Description: types.StringValue("no extras"), - Disk: types.Int64Value(10), - Ram: types.Int64Value(1024), - Vcpus: types.Int64Value(1), - ExtraSpecs: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - expectError: false, - }, - { - name: "nil extrasSpecs should return null map", - args: args{ - initial: DataSourceModel{ - ProjectId: types.StringValue("pid-987"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("g1.nil"), - Description: utils.Ptr("missing extras"), - Disk: utils.Ptr(int64(40)), - Ram: utils.Ptr(int64(8096)), - Vcpus: utils.Ptr(int64(4)), - ExtraSpecs: nil, - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid-987,eu01,g1.nil"), - ProjectId: types.StringValue("pid-987"), - Name: types.StringValue("g1.nil"), - Description: types.StringValue("missing extras"), - Disk: types.Int64Value(40), - Ram: types.Int64Value(8096), - Vcpus: types.Int64Value(4), - ExtraSpecs: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - expectError: false, - }, - { - name: "invalid extraSpecs with non-string values", - args: args{ - initial: DataSourceModel{ - ProjectId: types.StringValue("test-err"), - }, - input: &iaas.MachineType{ - Name: utils.Ptr("invalid"), - Description: utils.Ptr("bad map"), - Disk: utils.Ptr(int64(10)), - Ram: utils.Ptr(int64(4096)), - Vcpus: utils.Ptr(int64(2)), - ExtraSpecs: &map[string]interface{}{ - "cpu": "intel", - "burst": true, // not a string - "gen": 8, // not a string - }, - }, - }, - expected: DataSourceModel{}, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.initial, tt.args.region) - if tt.expectError { - if err == nil { - t.Errorf("expected error but got none") - } - return - } - - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - diff := cmp.Diff(tt.expected, tt.args.initial) - if diff != "" { - t.Errorf("unexpected diff (-want +got):\n%s", diff) - } - - // Extra sanity check for proper ID format - if id := tt.args.initial.Id.ValueString(); !strings.HasPrefix(id, tt.args.initial.ProjectId.ValueString()+",") { - t.Errorf("unexpected ID format: got %q", id) - } - }) - } -} - -func TestSortMachineTypeByName(t *testing.T) { - tests := []struct { - name string - input []*iaas.MachineType - ascending bool - expected []string - expectError bool - }{ - { - name: "ascending order", - input: []*iaas.MachineType{{Name: utils.Ptr("zeta")}, {Name: utils.Ptr("alpha")}, {Name: utils.Ptr("gamma")}}, - ascending: true, - expected: []string{"alpha", "gamma", "zeta"}, - }, - { - name: "descending order", - input: []*iaas.MachineType{{Name: utils.Ptr("zeta")}, {Name: utils.Ptr("alpha")}, {Name: utils.Ptr("gamma")}}, - ascending: false, - expected: []string{"zeta", "gamma", "alpha"}, - }, - { - name: "handles nil names", - input: []*iaas.MachineType{{Name: utils.Ptr("beta")}, nil, {Name: nil}, {Name: utils.Ptr("alpha")}}, - ascending: true, - expected: []string{"alpha", "beta"}, - }, - { - name: "empty input", - input: []*iaas.MachineType{}, - ascending: true, - expected: nil, - expectError: false, - }, - { - name: "nil input", - input: nil, - ascending: true, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sorted, err := sortMachineTypeByName(tt.input, tt.ascending) - - if tt.expectError { - if err == nil { - t.Errorf("expected error but got none") - } - return - } - - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - var result []string - for _, mt := range sorted { - if mt.Name != nil { - result = append(result, *mt.Name) - } - } - - if diff := cmp.Diff(tt.expected, result); diff != "" { - t.Errorf("unexpected sorted order (-want +got):\n%s", diff) - } - }) - } -} diff --git a/stackit/internal/services/iaas/network/datasource.go b/stackit/internal/services/iaas/network/datasource.go deleted file mode 100644 index 4197ee1f..00000000 --- a/stackit/internal/services/iaas/network/datasource.go +++ /dev/null @@ -1,402 +0,0 @@ -package network - -import ( - "context" - "fmt" - "net" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &networkDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - NetworkId types.String `tfsdk:"network_id"` - Name types.String `tfsdk:"name"` - Nameservers types.List `tfsdk:"nameservers"` - IPv4Gateway types.String `tfsdk:"ipv4_gateway"` - IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` - IPv4Prefix types.String `tfsdk:"ipv4_prefix"` - IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` - Prefixes types.List `tfsdk:"prefixes"` - IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` - IPv6Gateway types.String `tfsdk:"ipv6_gateway"` - IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` - IPv6Prefix types.String `tfsdk:"ipv6_prefix"` - IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` - IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` - PublicIP types.String `tfsdk:"public_ip"` - Labels types.Map `tfsdk:"labels"` - Routed types.Bool `tfsdk:"routed"` - Region types.String `tfsdk:"region"` - RoutingTableID types.String `tfsdk:"routing_table_id"` -} - -// NewNetworkDataSource is a helper function to simplify the provider implementation. -func NewNetworkDataSource() datasource.DataSource { - return &networkDataSource{} -} - -// networkDataSource is the data source implementation. -type networkDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *networkDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network" -} - -func (d *networkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Network resource schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the network is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_id": schema.StringAttribute{ - Description: "The network ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the network.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "nameservers": schema.ListAttribute{ - Description: "The nameservers of the network. This field is deprecated and will be removed soon, use `ipv4_nameservers` to configure the nameservers for IPv4.", - DeprecationMessage: "Use `ipv4_nameservers` to configure the nameservers for IPv4.", - Computed: true, - ElementType: types.StringType, - }, - "ipv4_gateway": schema.StringAttribute{ - Description: "The IPv4 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway.", - Computed: true, - }, - "ipv4_nameservers": schema.ListAttribute{ - Description: "The IPv4 nameservers of the network.", - Computed: true, - ElementType: types.StringType, - }, - "ipv4_prefix": schema.StringAttribute{ - Description: "The IPv4 prefix of the network (CIDR).", - DeprecationMessage: "The API supports reading multiple prefixes. So using the attribute 'ipv4_prefixes` should be preferred. This attribute will be populated with the first element from the list", - Computed: true, - }, - "ipv4_prefix_length": schema.Int64Attribute{ - Description: "The IPv4 prefix length of the network.", - Computed: true, - }, - "prefixes": schema.ListAttribute{ - Description: "The prefixes of the network. This field is deprecated and will be removed soon, use `ipv4_prefixes` to read the prefixes of the IPv4 networks.", - DeprecationMessage: "Use `ipv4_prefixes` to read the prefixes of the IPv4 networks.", - Computed: true, - ElementType: types.StringType, - }, - "ipv4_prefixes": schema.ListAttribute{ - Description: "The IPv4 prefixes of the network.", - Computed: true, - ElementType: types.StringType, - }, - "ipv6_gateway": schema.StringAttribute{ - Description: "The IPv6 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway.", - Computed: true, - }, - "ipv6_nameservers": schema.ListAttribute{ - Description: "The IPv6 nameservers of the network.", - Computed: true, - ElementType: types.StringType, - }, - "ipv6_prefix": schema.StringAttribute{ - Description: "The IPv6 prefix of the network (CIDR).", - DeprecationMessage: "The API supports reading multiple prefixes. So using the attribute 'ipv6_prefixes` should be preferred. This attribute will be populated with the first element from the list", - Computed: true, - }, - "ipv6_prefix_length": schema.Int64Attribute{ - Description: "The IPv6 prefix length of the network.", - Computed: true, - }, - "ipv6_prefixes": schema.ListAttribute{ - Description: "The IPv6 prefixes of the network.", - Computed: true, - ElementType: types.StringType, - }, - "public_ip": schema.StringAttribute{ - Description: "The public IP of the network.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - "routed": schema.BoolAttribute{ - Description: "Shows if the network is routed and therefore accessible from other networks.", - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: "Can only be used when experimental \"network\" is set. This is likely going to undergo significant changes or be removed in the future.\nThe resource region. If not defined, the provider region is used.", - }, - "routing_table_id": schema.StringAttribute{ - Description: "Can only be used when experimental \"network\" is set. This is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.\nThe ID of the routing table associated with the network.", - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - - networkResp, err := d.client.GetNetwork(ctx, projectId, region, networkId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading network", - fmt.Sprintf("Network with ID %q does not exist in project %q.", networkId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - err = mapDataSourceFields(ctx, networkResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network read") -} - -func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model *DataSourceModel, region string) error { - if networkResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkId string - if model.NetworkId.ValueString() != "" { - networkId = model.NetworkId.ValueString() - } else if networkResp.Id != nil { - networkId = *networkResp.Id - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // IPv4 - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil { - model.Nameservers = types.ListNull(types.StringType) - model.IPv4Nameservers = types.ListNull(types.StringType) - } else { - respNameservers := *networkResp.Ipv4.Nameservers - modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) - modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) - if err != nil { - return fmt.Errorf("get current network nameservers from model: %w", err) - } - if errIpv4 != nil { - return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) - } - - reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) - reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) - - nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) - ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) - if diags.HasError() { - return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) - } - if ipv4Diags.HasError() { - return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) - } - - model.Nameservers = nameserversTF - model.IPv4Nameservers = ipv4NameserversTF - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil { - model.Prefixes = types.ListNull(types.StringType) - model.IPv4Prefixes = types.ListNull(types.StringType) - } else { - respPrefixes := *networkResp.Ipv4.Prefixes - prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) - if diags.HasError() { - return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixes) > 0 { - model.IPv4Prefix = types.StringValue(respPrefixes[0]) - _, netmask, err := net.ParseCIDR(respPrefixes[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv4PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv4PrefixLength = types.Int64Value(int64(ones)) - } - } - - model.Prefixes = prefixesTF - model.IPv4Prefixes = prefixesTF - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil { - model.IPv4Gateway = types.StringNull() - } else { - model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway()) - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil { - model.PublicIP = types.StringNull() - } else { - model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp) - } - - // IPv6 - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil { - model.IPv6Nameservers = types.ListNull(types.StringType) - } else { - respIPv6Nameservers := *networkResp.Ipv6.Nameservers - modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) - if errIpv6 != nil { - return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) - } - - reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) - - ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) - if ipv6Diags.HasError() { - return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) - } - - model.IPv6Nameservers = ipv6NameserversTF - } - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil { - model.IPv6Prefixes = types.ListNull(types.StringType) - } else { - respPrefixesV6 := *networkResp.Ipv6.Prefixes - prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) - if diags.HasError() { - return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixesV6) > 0 { - model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) - _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv6PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv6PrefixLength = types.Int64Value(int64(ones)) - } - } - model.IPv6Prefixes = prefixesV6TF - } - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil { - model.IPv6Gateway = types.StringNull() - } else { - model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway()) - } - - model.RoutingTableID = types.StringNull() - if networkResp.RoutingTableId != nil { - model.RoutingTableID = types.StringValue(*networkResp.RoutingTableId) - } - - model.NetworkId = types.StringValue(networkId) - model.Name = types.StringPointerValue(networkResp.Name) - model.Labels = labels - model.Routed = types.BoolPointerValue(networkResp.Routed) - model.Region = types.StringValue(region) - - return nil -} diff --git a/stackit/internal/services/iaas/network/datasource_test.go b/stackit/internal/services/iaas/network/datasource_test.go deleted file mode 100644 index c7c4d7f9..00000000 --- a/stackit/internal/services/iaas/network/datasource_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package network - -import ( - "context" - "testing" - - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" -) - -const ( - testRegion = "region" -) - -func TestMapDataSourceFields(t *testing.T) { - tests := []struct { - description string - state DataSourceModel - input *iaas.Network - region string - expected DataSourceModel - isValid bool - }{ - { - "id_ok", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv4: &iaas.NetworkIPv4{ - Gateway: iaas.NewNullableString(nil), - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - IPv4Prefix: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefix: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "values_ok", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Name: utils.Ptr("name"), - Ipv4: &iaas.NetworkIPv4{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Prefixes: &[]string{ - "192.168.42.0/24", - "10.100.10.0/16", - }, - PublicIp: utils.Ptr("publicIp"), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - Ipv6: &iaas.NetworkIPv6{ - Nameservers: &[]string{ - "ns1", - "ns2", - }, - Prefixes: &[]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789a:2::/64", - }, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(true), - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefix: types.StringValue("192.168.42.0/24"), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - PublicIP: types.StringValue("publicIp"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - IPv6Gateway: types.StringValue("gateway"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv4_nameservers_changed_outside_tf", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv4: &iaas.NetworkIPv4{ - Nameservers: &[]string{ - "ns2", - "ns3", - }, - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv6_nameservers_changed_outside_tf", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv6: &iaas.NetworkIPv6{ - Nameservers: &[]string{ - "ns2", - "ns3", - }, - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv4_prefixes_changed_outside_tf", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv4: &iaas.NetworkIPv4{ - Prefixes: &[]string{ - "10.100.20.0/16", - "10.100.10.0/16", - }, - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Value(16), - IPv4Prefix: types.StringValue("10.100.20.0/16"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("10.100.20.0/16"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("10.100.20.0/16"), - types.StringValue("10.100.10.0/16"), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv6_prefixes_changed_outside_tf", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv6: &iaas.NetworkIPv6{ - Prefixes: &[]string{ - "fd12:3456:789a:3::/64", - "fd12:3456:789a:4::/64", - }, - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefix: types.StringValue("fd12:3456:789a:3::/64"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:3::/64"), - types.StringValue("fd12:3456:789a:4::/64"), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv4_ipv6_gateway_nil", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "response_nil_fail", - DataSourceModel{}, - nil, - testRegion, - DataSourceModel{}, - false, - }, - { - "no_resource_id", - DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - &iaas.Network{}, - testRegion, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.input, &tt.state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/network/resource.go b/stackit/internal/services/iaas/network/resource.go deleted file mode 100644 index 0665a3bb..00000000 --- a/stackit/internal/services/iaas/network/resource.go +++ /dev/null @@ -1,956 +0,0 @@ -package network - -import ( - "context" - "fmt" - "net" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "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/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &networkResource{} - _ resource.ResourceWithConfigure = &networkResource{} - _ resource.ResourceWithImportState = &networkResource{} - _ resource.ResourceWithModifyPlan = &networkResource{} -) - -const ( - ipv4BehaviorChangeTitle = "Behavior of not configured `ipv4_nameservers` will change from January 2026" - ipv4BehaviorChangeDescription = "When `ipv4_nameservers` is not set, it will be set to the network area's `default_nameservers`.\n" + - "To prevent any nameserver configuration, the `ipv4_nameservers` attribute should be explicitly set to an empty list `[]`.\n" + - "In cases where `ipv4_nameservers` are defined within the resource, the existing behavior will remain unchanged." -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - NetworkId types.String `tfsdk:"network_id"` - Name types.String `tfsdk:"name"` - Nameservers types.List `tfsdk:"nameservers"` - IPv4Gateway types.String `tfsdk:"ipv4_gateway"` - IPv4Nameservers types.List `tfsdk:"ipv4_nameservers"` - IPv4Prefix types.String `tfsdk:"ipv4_prefix"` - IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"` - Prefixes types.List `tfsdk:"prefixes"` - IPv4Prefixes types.List `tfsdk:"ipv4_prefixes"` - IPv6Gateway types.String `tfsdk:"ipv6_gateway"` - IPv6Nameservers types.List `tfsdk:"ipv6_nameservers"` - IPv6Prefix types.String `tfsdk:"ipv6_prefix"` - IPv6PrefixLength types.Int64 `tfsdk:"ipv6_prefix_length"` - IPv6Prefixes types.List `tfsdk:"ipv6_prefixes"` - PublicIP types.String `tfsdk:"public_ip"` - Labels types.Map `tfsdk:"labels"` - Routed types.Bool `tfsdk:"routed"` - NoIPv4Gateway types.Bool `tfsdk:"no_ipv4_gateway"` - NoIPv6Gateway types.Bool `tfsdk:"no_ipv6_gateway"` - Region types.String `tfsdk:"region"` - RoutingTableID types.String `tfsdk:"routing_table_id"` -} - -// NewNetworkResource is a helper function to simplify the provider implementation. -func NewNetworkResource() resource.Resource { - return &networkResource{} -} - -// networkResource is the resource implementation. -type networkResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *networkResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network" -} - -// Configure adds the provider configured client to the resource. -func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - // Warning should only be shown during the plan of the creation. This can be detected by checking if the ID is set. - if utils.IsUndefined(planModel.Id) && utils.IsUndefined(planModel.IPv4Nameservers) { - addIPv4Warning(&resp.Diagnostics) - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (r *networkResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var resourceModel Model - resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) - if resp.Diagnostics.HasError() { - return - } - - if !resourceModel.Nameservers.IsUnknown() && !resourceModel.IPv4Nameservers.IsUnknown() && !resourceModel.Nameservers.IsNull() && !resourceModel.IPv4Nameservers.IsNull() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "You cannot provide both the `nameservers` and `ipv4_nameservers` fields simultaneously. Please remove the deprecated `nameservers` field, and use `ipv4_nameservers` to configure nameservers for IPv4.") - } -} - -// ConfigValidators validates the resource configuration -func (r *networkResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { - return []resource.ConfigValidator{ - resourcevalidator.Conflicting( - path.MatchRoot("no_ipv4_gateway"), - path.MatchRoot("ipv4_gateway"), - ), - resourcevalidator.Conflicting( - path.MatchRoot("no_ipv6_gateway"), - path.MatchRoot("ipv6_gateway"), - ), - resourcevalidator.Conflicting( - path.MatchRoot("ipv4_prefix"), - path.MatchRoot("ipv4_prefix_length"), - ), - resourcevalidator.Conflicting( - path.MatchRoot("ipv6_prefix"), - path.MatchRoot("ipv6_prefix_length"), - ), - resourcevalidator.Conflicting( - path.MatchRoot("ipv4_prefix_length"), - path.MatchRoot("ipv4_gateway"), - ), - resourcevalidator.Conflicting( - path.MatchRoot("ipv6_prefix_length"), - path.MatchRoot("ipv6_gateway"), - ), - } -} - -// Schema defines the schema for the resource. -func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network resource schema. Must have a `region` specified in the provider configuration." - descriptionNote := fmt.Sprintf("~> %s. %s", ipv4BehaviorChangeTitle, ipv4BehaviorChangeDescription) - resp.Schema = schema.Schema{ - MarkdownDescription: fmt.Sprintf("%s\n%s", description, descriptionNote), - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`network_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the network is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_id": schema.StringAttribute{ - Description: "The network ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the network.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "nameservers": schema.ListAttribute{ - Description: "The nameservers of the network. This field is deprecated and will be removed in January 2026, use `ipv4_nameservers` to configure the nameservers for IPv4.", - DeprecationMessage: "Use `ipv4_nameservers` to configure the nameservers for IPv4.", - Optional: true, - Computed: true, - ElementType: types.StringType, - }, - "no_ipv4_gateway": schema.BoolAttribute{ - Description: "If set to `true`, the network doesn't have a gateway.", - Optional: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "ipv4_gateway": schema.StringAttribute{ - Description: "The IPv4 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway.", - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.IP(false), - }, - }, - "ipv4_nameservers": schema.ListAttribute{ - Description: "The IPv4 nameservers of the network.", - Optional: true, - Computed: true, - ElementType: types.StringType, - }, - "ipv4_prefix": schema.StringAttribute{ - Description: "The IPv4 prefix of the network (CIDR).", - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.CIDR(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplaceIfConfigured(), - }, - }, - "ipv4_prefix_length": schema.Int64Attribute{ - Description: "The IPv4 prefix length of the network.", - Computed: true, - Optional: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplaceIfConfigured(), - }, - }, - "prefixes": schema.ListAttribute{ - Description: "The prefixes of the network. This field is deprecated and will be removed in January 2026, use `ipv4_prefixes` to read the prefixes of the IPv4 networks.", - DeprecationMessage: "Use `ipv4_prefixes` to read the prefixes of the IPv4 networks.", - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "ipv4_prefixes": schema.ListAttribute{ - Description: "The IPv4 prefixes of the network.", - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "no_ipv6_gateway": schema.BoolAttribute{ - Description: "If set to `true`, the network doesn't have a gateway.", - Optional: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "ipv6_gateway": schema.StringAttribute{ - Description: "The IPv6 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway.", - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.IP(false), - }, - }, - "ipv6_nameservers": schema.ListAttribute{ - Description: "The IPv6 nameservers of the network.", - Optional: true, - Computed: true, - ElementType: types.StringType, - }, - "ipv6_prefix": schema.StringAttribute{ - Description: "The IPv6 prefix of the network (CIDR).", - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.CIDR(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "ipv6_prefix_length": schema.Int64Attribute{ - Description: "The IPv6 prefix length of the network.", - Optional: true, - Computed: true, - }, - "ipv6_prefixes": schema.ListAttribute{ - Description: "The IPv6 prefixes of the network.", - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "public_ip": schema.StringAttribute{ - Description: "The public IP of the network.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - "routed": schema.BoolAttribute{ - Description: "If set to `true`, the network is routed and therefore accessible from other networks.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - boolplanmodifier.RequiresReplace(), - }, - }, - "routing_table_id": schema.StringAttribute{ - Description: "The ID of the routing table associated with the network.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplaceIfConfigured(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // When IPv4Nameserver is not set, print warning that the behavior of ipv4_nameservers will change - if utils.IsUndefined(model.IPv4Nameservers) { - addIPv4Warning(&resp.Diagnostics) - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network - - network, err := r.client.CreateNetwork(ctx, projectId, region).CreateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Calling API: %v", err)) - return - } - - networkId := *network.Id - ctx = tflog.SetField(ctx, "network_id", networkId) - - network, err = wait.CreateNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, network, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *networkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "region", region) - - networkResp, err := r.client.GetNetwork(ctx, projectId, region, networkId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, networkResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "region", region) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, &stateModel) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing network - err = r.client.PartialUpdateNetwork(ctx, projectId, region, networkId).PartialUpdateNetworkPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) - return - } - waitResp, err := wait.UpdateNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) - return - } - - err = mapFields(ctx, waitResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *networkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - networkId := model.NetworkId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing network - err := r.client.DeleteNetwork(ctx, projectId, region, networkId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) - return - } - _, err = wait.DeleteNetworkWaitHandler(ctx, r.client, projectId, region, networkId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Network deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,region,network_id -func (r *networkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id] Got: %q", req.ID), - ) - return - } - - projectId := idParts[0] - region := idParts[1] - networkId := idParts[2] - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_id", networkId) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...) - tflog.Info(ctx, "Network state imported") -} - -func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model, region string) error { - if networkResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkId string - if model.NetworkId.ValueString() != "" { - networkId = model.NetworkId.ValueString() - } else if networkResp.Id != nil { - networkId = *networkResp.Id - } else { - return fmt.Errorf("network id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, networkId) - - labels, err := iaasUtils.MapLabels(ctx, networkResp.Labels, model.Labels) - if err != nil { - return err - } - - // IPv4 - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Nameservers == nil { - model.Nameservers = types.ListNull(types.StringType) - model.IPv4Nameservers = types.ListNull(types.StringType) - } else { - respNameservers := *networkResp.Ipv4.Nameservers - modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) - modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) - if err != nil { - return fmt.Errorf("get current network nameservers from model: %w", err) - } - if errIpv4 != nil { - return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) - } - - reconciledNameservers := utils.ReconcileStringSlices(modelNameservers, respNameservers) - reconciledIPv4Nameservers := utils.ReconcileStringSlices(modelIPv4Nameservers, respNameservers) - - nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) - ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) - if diags.HasError() { - return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) - } - if ipv4Diags.HasError() { - return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) - } - - model.Nameservers = nameserversTF - model.IPv4Nameservers = ipv4NameserversTF - } - - model.IPv4PrefixLength = types.Int64Null() - if networkResp.Ipv4 == nil || networkResp.Ipv4.Prefixes == nil { - model.Prefixes = types.ListNull(types.StringType) - model.IPv4Prefixes = types.ListNull(types.StringType) - } else { - respPrefixes := *networkResp.Ipv4.Prefixes - prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) - if diags.HasError() { - return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixes) > 0 { - model.IPv4Prefix = types.StringValue(respPrefixes[0]) - _, netmask, err := net.ParseCIDR(respPrefixes[0]) - if err != nil { - tflog.Error(ctx, fmt.Sprintf("ipv4_prefix_length: %+v", err)) - // silently ignore parsing error for the netmask - model.IPv4PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv4PrefixLength = types.Int64Value(int64(ones)) - } - } - - model.Prefixes = prefixesTF - model.IPv4Prefixes = prefixesTF - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.Gateway == nil { - model.IPv4Gateway = types.StringNull() - } else { - model.IPv4Gateway = types.StringPointerValue(networkResp.Ipv4.GetGateway()) - } - - if networkResp.Ipv4 == nil || networkResp.Ipv4.PublicIp == nil { - model.PublicIP = types.StringNull() - } else { - model.PublicIP = types.StringPointerValue(networkResp.Ipv4.PublicIp) - } - - // IPv6 - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Nameservers == nil { - model.IPv6Nameservers = types.ListNull(types.StringType) - } else { - respIPv6Nameservers := *networkResp.Ipv6.Nameservers - modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) - if errIpv6 != nil { - return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) - } - - reconciledIPv6Nameservers := utils.ReconcileStringSlices(modelIPv6Nameservers, respIPv6Nameservers) - - ipv6NameserversTF, ipv6Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv6Nameservers) - if ipv6Diags.HasError() { - return fmt.Errorf("map IPv6 network nameservers: %w", core.DiagsToError(ipv6Diags)) - } - - model.IPv6Nameservers = ipv6NameserversTF - } - - model.IPv6PrefixLength = types.Int64Null() - model.IPv6Prefix = types.StringNull() - if networkResp.Ipv6 == nil || networkResp.Ipv6.Prefixes == nil { - model.IPv6Prefixes = types.ListNull(types.StringType) - } else { - respPrefixesV6 := *networkResp.Ipv6.Prefixes - prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) - if diags.HasError() { - return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) - } - if len(respPrefixesV6) > 0 { - model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) - _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) - if err != nil { - // silently ignore parsing error for the netmask - model.IPv6PrefixLength = types.Int64Null() - } else { - ones, _ := netmask.Mask.Size() - model.IPv6PrefixLength = types.Int64Value(int64(ones)) - } - } - model.IPv6Prefixes = prefixesV6TF - } - - if networkResp.Ipv6 == nil || networkResp.Ipv6.Gateway == nil { - model.IPv6Gateway = types.StringNull() - } else { - model.IPv6Gateway = types.StringPointerValue(networkResp.Ipv6.GetGateway()) - } - - model.RoutingTableID = types.StringPointerValue(networkResp.RoutingTableId) - model.NetworkId = types.StringValue(networkId) - model.Name = types.StringPointerValue(networkResp.Name) - model.Labels = labels - model.Routed = types.BoolPointerValue(networkResp.Routed) - model.Region = types.StringValue(region) - - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var modelIPv6Nameservers []string - // Is true when IPv6Nameservers is not null or unset - if !utils.IsUndefined(model.IPv6Nameservers) { - // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. - // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set - modelIPv6Nameservers = []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) - } - } - - var ipv6Body *iaas.CreateNetworkIPv6 - if !utils.IsUndefined(model.IPv6PrefixLength) { - ipv6Body = &iaas.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefixLength: &iaas.CreateNetworkIPv6WithPrefixLength{ - PrefixLength: conversion.Int64ValueToPointer(model.IPv6PrefixLength), - }, - } - - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - ipv6Body.CreateNetworkIPv6WithPrefixLength.Nameservers = &modelIPv6Nameservers - } - } else if !utils.IsUndefined(model.IPv6Prefix) { - var gateway *iaas.NullableString - if model.NoIPv6Gateway.ValueBool() { - gateway = iaas.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { - gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) - } - - ipv6Body = &iaas.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ - Gateway: gateway, - Prefix: conversion.StringValueToPointer(model.IPv6Prefix), - }, - } - - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - ipv6Body.CreateNetworkIPv6WithPrefix.Nameservers = &modelIPv6Nameservers - } - } - - modelIPv4Nameservers := []string{} - var modelIPv4List []attr.Value - - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { - modelIPv4List = model.IPv4Nameservers.Elements() - } else { - modelIPv4List = model.Nameservers.Elements() - } - - for _, ipv4ns := range modelIPv4List { - ipv4NameserverString, ok := ipv4ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) - } - - var ipv4Body *iaas.CreateNetworkIPv4 - if !utils.IsUndefined(model.IPv4PrefixLength) { - ipv4Body = &iaas.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefixLength: &iaas.CreateNetworkIPv4WithPrefixLength{ - Nameservers: &modelIPv4Nameservers, - PrefixLength: conversion.Int64ValueToPointer(model.IPv4PrefixLength), - }, - } - } else if !utils.IsUndefined(model.IPv4Prefix) { - var gateway *iaas.NullableString - if model.NoIPv4Gateway.ValueBool() { - gateway = iaas.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { - gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) - } - - ipv4Body = &iaas.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ - Nameservers: &modelIPv4Nameservers, - Prefix: conversion.StringValueToPointer(model.IPv4Prefix), - Gateway: gateway, - }, - } - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - payload := iaas.CreateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - Routed: conversion.BoolValueToPointer(model.Routed), - Ipv4: ipv4Body, - Ipv6: ipv6Body, - RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), - } - - return &payload, nil -} - -func toUpdatePayload(ctx context.Context, model, stateModel *Model) (*iaas.PartialUpdateNetworkPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var modelIPv6Nameservers []string - // Is true when IPv6Nameservers is not null or unset - if !utils.IsUndefined(model.IPv6Nameservers) { - // If ipv6Nameservers is empty, modelIPv6Nameservers will be set to an empty slice. - // empty slice != nil slice. Empty slice will result in an empty list in the payload []. Nil slice will result in a payload without the property set - modelIPv6Nameservers = []string{} - for _, ipv6ns := range model.IPv6Nameservers.Elements() { - ipv6NameserverString, ok := ipv6ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) - } - } - - var ipv6Body *iaas.UpdateNetworkIPv6Body - if modelIPv6Nameservers != nil || !utils.IsUndefined(model.NoIPv6Gateway) || !utils.IsUndefined(model.IPv6Gateway) { - ipv6Body = &iaas.UpdateNetworkIPv6Body{} - // IPv6 nameservers should only be set, if it contains any value. If the slice is nil, it should NOT be set. - // Setting it to a nil slice would result in a payload, where nameservers is set to null in the json payload, - // but it should actually be unset. Setting it to "null" will result in an error, because it's NOT nullable. - if modelIPv6Nameservers != nil { - ipv6Body.Nameservers = &modelIPv6Nameservers - } - - if model.NoIPv6Gateway.ValueBool() { - ipv6Body.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { - ipv6Body.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) - } - } - - modelIPv4Nameservers := []string{} - var modelIPv4List []attr.Value - - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { - modelIPv4List = model.IPv4Nameservers.Elements() - } else { - modelIPv4List = model.Nameservers.Elements() - } - for _, ipv4ns := range modelIPv4List { - ipv4NameserverString, ok := ipv4ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) - } - - var ipv4Body *iaas.UpdateNetworkIPv4Body - if !model.IPv4Nameservers.IsNull() || !model.Nameservers.IsNull() { - ipv4Body = &iaas.UpdateNetworkIPv4Body{ - Nameservers: &modelIPv4Nameservers, - } - - if model.NoIPv4Gateway.ValueBool() { - ipv4Body.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { - ipv4Body.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) - } - } - currentLabels := stateModel.Labels - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - payload := iaas.PartialUpdateNetworkPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - Ipv4: ipv4Body, - Ipv6: ipv6Body, - RoutingTableId: conversion.StringValueToPointer(model.RoutingTableID), - } - - return &payload, nil -} - -func addIPv4Warning(diags *diag.Diagnostics) { - diags.AddAttributeWarning(path.Root("ipv4_nameservers"), - ipv4BehaviorChangeTitle, - ipv4BehaviorChangeDescription) -} diff --git a/stackit/internal/services/iaas/network/resource_test.go b/stackit/internal/services/iaas/network/resource_test.go deleted file mode 100644 index 929424d6..00000000 --- a/stackit/internal/services/iaas/network/resource_test.go +++ /dev/null @@ -1,818 +0,0 @@ -package network - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - state Model - input *iaas.Network - region string - expected Model - isValid bool - }{ - { - "id_ok", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv4: &iaas.NetworkIPv4{ - Gateway: iaas.NewNullableString(nil), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - IPv4Prefix: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefix: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "values_ok", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Name: utils.Ptr("name"), - Ipv4: &iaas.NetworkIPv4{ - Nameservers: utils.Ptr([]string{"ns1", "ns2"}), - Prefixes: utils.Ptr( - []string{ - "192.168.42.0/24", - "10.100.10.0/16", - }, - ), - PublicIp: utils.Ptr("publicIp"), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - Ipv6: &iaas.NetworkIPv6{ - Nameservers: utils.Ptr([]string{"ns1", "ns2"}), - Prefixes: utils.Ptr([]string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789b:1::/64", - }), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(true), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4PrefixLength: types.Int64Value(24), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/16"), - }), - IPv4Prefix: types.StringValue("192.168.42.0/24"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789b:1::/64"), - }), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - PublicIP: types.StringValue("publicIp"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - IPv6Gateway: types.StringValue("gateway"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv4_nameservers_changed_outside_tf", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv4: &iaas.NetworkIPv4{ - Nameservers: utils.Ptr([]string{ - "ns2", - "ns3", - }), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv6_nameservers_changed_outside_tf", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv6: &iaas.NetworkIPv6{ - Nameservers: utils.Ptr([]string{ - "ns2", - "ns3", - }), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns2"), - types.StringValue("ns3"), - }), - Labels: types.MapNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv4_prefixes_changed_outside_tf", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.42.0/24"), - types.StringValue("10.100.10.0/24"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv4: &iaas.NetworkIPv4{ - Prefixes: utils.Ptr( - []string{ - "192.168.54.0/24", - "192.168.55.0/24", - }, - ), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Value(24), - IPv4Prefix: types.StringValue("192.168.54.0/24"), - Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.54.0/24"), - types.StringValue("192.168.55.0/24"), - }), - IPv4Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("192.168.54.0/24"), - types.StringValue("192.168.55.0/24"), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv6_prefixes_changed_outside_tf", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - Ipv6: &iaas.NetworkIPv6{ - Prefixes: utils.Ptr( - []string{ - "fd12:3456:789a:1::/64", - "fd12:3456:789a:2::/64", - }, - ), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - Nameservers: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Value(64), - IPv6Prefixes: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("fd12:3456:789a:1::/64"), - types.StringValue("fd12:3456:789a:2::/64"), - }), - IPv6Prefix: types.StringValue("fd12:3456:789a:1::/64"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "ipv4_ipv6_gateway_nil", - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - }, - &iaas.Network{ - Id: utils.Ptr("nid"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,nid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Name: types.StringNull(), - Nameservers: types.ListNull(types.StringType), - IPv4Nameservers: types.ListNull(types.StringType), - IPv4PrefixLength: types.Int64Null(), - IPv4Gateway: types.StringNull(), - Prefixes: types.ListNull(types.StringType), - IPv4Prefixes: types.ListNull(types.StringType), - IPv6Nameservers: types.ListNull(types.StringType), - IPv6PrefixLength: types.Int64Null(), - IPv6Gateway: types.StringNull(), - IPv6Prefixes: types.ListNull(types.StringType), - PublicIP: types.StringNull(), - Labels: types.MapNull(types.StringType), - Routed: types.BoolNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "response_nil_fail", - Model{}, - nil, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - ProjectId: types.StringValue("pid"), - }, - &iaas.Network{}, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateNetworkPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv4Gateway: types.StringValue("gateway"), - IPv4Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv4: &iaas.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv4_nameservers_okay", - &Model{ - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv4Gateway: types.StringValue("gateway"), - IPv4Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv4: &iaas.CreateNetworkIPv4{ - CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_default_ok", - &Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv6: &iaas.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_nameserver_null", - &Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListNull(types.StringType), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv6: &iaas.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ - Nameservers: nil, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - { - "ipv6_nameserver_empty_list", - &Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(false), - IPv6Gateway: types.StringValue("gateway"), - IPv6Prefix: types.StringValue("prefix"), - }, - &iaas.CreateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv6: &iaas.CreateNetworkIPv6{ - CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ - Nameservers: utils.Ptr([]string{}), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Prefix: utils.Ptr("prefix"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - Routed: utils.Ptr(false), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - state Model - expected *iaas.PartialUpdateNetworkPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - }, - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv4_nameservers_okay", - &Model{ - Name: types.StringValue("name"), - Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv4Gateway: types.StringValue("gateway"), - }, - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv4_gateway_nil", - &Model{ - Name: types.StringValue("name"), - IPv4Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - }, - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_default_ok", - &Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_gateway_nil", - &Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - }, - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: utils.Ptr([]string{ - "ns1", - "ns2", - }), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_nameserver_null", - &Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListNull(types.StringType), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: nil, - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - { - "ipv6_nameserver_empty_list", - &Model{ - Name: types.StringValue("name"), - IPv6Nameservers: types.ListValueMust(types.StringType, []attr.Value{}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Routed: types.BoolValue(true), - IPv6Gateway: types.StringValue("gateway"), - }, - Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - Labels: types.MapNull(types.StringType), - }, - &iaas.PartialUpdateNetworkPayload{ - Name: utils.Ptr("name"), - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: utils.Ptr([]string{}), - Gateway: iaas.NewNullableString(utils.Ptr("gateway")), - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/networkarea/datasource.go b/stackit/internal/services/iaas/networkarea/datasource.go deleted file mode 100644 index 75edfd2a..00000000 --- a/stackit/internal/services/iaas/networkarea/datasource.go +++ /dev/null @@ -1,242 +0,0 @@ -package networkarea - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &networkAreaDataSource{} -) - -// NewNetworkDataSource is a helper function to simplify the provider implementation. -func NewNetworkAreaDataSource() datasource.DataSource { - return &networkAreaDataSource{} -} - -// networkDataSource is the data source implementation. -type networkAreaDataSource struct { - client *iaas.APIClient -} - -// Metadata returns the data source type name. -func (d *networkAreaDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_area" -} - -func (d *networkAreaDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026." - description := "Network area datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`\".", - Computed: true, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the network area is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the network area.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "project_count": schema.Int64Attribute{ - Description: "The amount of projects currently referencing this area.", - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - }, - }, - "default_nameservers": schema.ListAttribute{ - DeprecationMessage: deprecationMsg, - Description: "List of DNS Servers/Nameservers.", - Computed: true, - ElementType: types.StringType, - }, - "network_ranges": schema.ListNestedAttribute{ - DeprecationMessage: deprecationMsg, - Description: "List of Network ranges.", - Computed: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - listvalidator.SizeAtMost(64), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "network_range_id": schema.StringAttribute{ - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "prefix": schema.StringAttribute{ - Computed: true, - }, - }, - }, - }, - "transfer_network": schema.StringAttribute{ - DeprecationMessage: deprecationMsg, - Description: "Classless Inter-Domain Routing (CIDR).", - Computed: true, - }, - "default_prefix_length": schema.Int64Attribute{ - DeprecationMessage: deprecationMsg, - Description: "The default prefix length for networks in the network area.", - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(24), - int64validator.AtMost(29), - }, - }, - "max_prefix_length": schema.Int64Attribute{ - DeprecationMessage: deprecationMsg, - Description: "The maximal prefix length for networks in the network area.", - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(24), - int64validator.AtMost(29), - }, - }, - "min_prefix_length": schema.Int64Attribute{ - DeprecationMessage: deprecationMsg, - Description: "The minimal prefix length for networks in the network area.", - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(22), - int64validator.AtMost(29), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - networkAreaResp, err := d.client.GetNetworkArea(ctx, organizationId, networkAreaId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading network area", - fmt.Sprintf("Network area with ID %q does not exist in organization %q.", networkAreaId, organizationId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, networkAreaResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - networkAreaRegionResp, err := d.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if !(ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest)) { // TODO: iaas api returns http 400 in case network area region is not found - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - networkAreaRegionResp = &iaas.RegionalArea{} - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area read") -} diff --git a/stackit/internal/services/iaas/networkarea/resource.go b/stackit/internal/services/iaas/networkarea/resource.go deleted file mode 100644 index c4b5cb56..00000000 --- a/stackit/internal/services/iaas/networkarea/resource.go +++ /dev/null @@ -1,1004 +0,0 @@ -package networkarea - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "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/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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -const ( - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - defaultValueDefaultPrefixLength = 25 - - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - defaultValueMinPrefixLength = 24 - - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - defaultValueMaxPrefixLength = 29 - - // Deprecated: Will be removed in May 2026. - deprecationWarningSummary = "Migration to new `stackit_network_area_region` resource needed" - // Deprecated: Will be removed in May 2026. - deprecationWarningDetails = "You're using deprecated features of the `stackit_network_area` resource. These will be removed in May 2026. Migrate to the new `stackit_network_area_region` resource instead." -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &networkAreaResource{} - _ resource.ResourceWithConfigure = &networkAreaResource{} - _ resource.ResourceWithImportState = &networkAreaResource{} - _ resource.ResourceWithValidateConfig = &networkAreaResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Name types.String `tfsdk:"name"` - ProjectCount types.Int64 `tfsdk:"project_count"` - Labels types.Map `tfsdk:"labels"` - - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - DefaultNameservers types.List `tfsdk:"default_nameservers"` - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"` - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - NetworkRanges types.List `tfsdk:"network_ranges"` - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - TransferNetwork types.String `tfsdk:"transfer_network"` - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"` - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"` -} - -// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. LegacyMode checks if any of the deprecated fields are set which now relate to the network area region API resource. -func (model *Model) LegacyMode() bool { - return !model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown() || !model.TransferNetwork.IsNull() || model.TransferNetwork.IsUnknown() || !model.DefaultNameservers.IsNull() || model.DefaultNameservers.IsUnknown() || model.DefaultPrefixLength != types.Int64Value(int64(defaultValueDefaultPrefixLength)) || model.MinPrefixLength != types.Int64Value(int64(defaultValueMinPrefixLength)) || model.MaxPrefixLength != types.Int64Value(int64(defaultValueMaxPrefixLength)) -} - -// Struct corresponding to Model.NetworkRanges[i] -type networkRange struct { - Prefix types.String `tfsdk:"prefix"` - NetworkRangeId types.String `tfsdk:"network_range_id"` -} - -// Types corresponding to networkRanges -var networkRangeTypes = map[string]attr.Type{ - "prefix": types.StringType, - "network_range_id": types.StringType, -} - -// NewNetworkAreaResource is a helper function to simplify the provider implementation. -func NewNetworkAreaResource() resource.Resource { - return &networkAreaResource{} -} - -// networkResource is the resource implementation. -type networkAreaResource struct { - client *iaas.APIClient - resourceManagerClient *resourcemanager.APIClient -} - -// Metadata returns the resource type name. -func (r *networkAreaResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_area" -} - -// Configure adds the provider configured client to the resource. -func (r *networkAreaResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - resourceManagerClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.resourceManagerClient = resourceManagerClient - tflog.Info(ctx, "IaaS client configured") -} - -// Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. -func (r *networkAreaResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var resourceModel Model - resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) - if resp.Diagnostics.HasError() { - return - } - - if resourceModel.NetworkRanges.IsNull() != resourceModel.TransferNetwork.IsNull() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to either provide both the `network_ranges` and `transfer_network` fields simultaneously or none of them.") - } - - if (resourceModel.NetworkRanges.IsNull() || resourceModel.TransferNetwork.IsNull()) && (!resourceModel.DefaultNameservers.IsNull() || !resourceModel.DefaultPrefixLength.IsNull() || !resourceModel.MinPrefixLength.IsNull() || !resourceModel.MaxPrefixLength.IsNull()) { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network network area", "You have to provide both the `network_ranges` and `transfer_network` fields when providing one of these fields: `default_nameservers`, `default_prefix_length`, `max_prefix_length`, `min_prefix_length`") - } -} - -// Schema defines the schema for the resource. -func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - deprecationMsg := "Deprecated because of the IaaS API v1 -> v2 migration. Will be removed in May 2026. Use the new `stackit_network_area_region` resource instead." - description := "Network area resource schema." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the network area is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the network area.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "project_count": schema.Int64Attribute{ - Description: "The amount of projects currently referencing this area.", - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - }, - }, - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - "default_nameservers": schema.ListAttribute{ - Description: "List of DNS Servers/Nameservers for configuration of network area for region `eu01`.", - DeprecationMessage: deprecationMsg, - Optional: true, - ElementType: types.StringType, - }, - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - "network_ranges": schema.ListNestedAttribute{ - Description: "List of Network ranges for configuration of network area for region `eu01`.", - DeprecationMessage: deprecationMsg, - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - listvalidator.SizeAtMost(64), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "network_range_id": schema.StringAttribute{ - DeprecationMessage: deprecationMsg, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "prefix": schema.StringAttribute{ - DeprecationMessage: deprecationMsg, - Description: "Classless Inter-Domain Routing (CIDR).", - Required: true, - }, - }, - }, - }, - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - "transfer_network": schema.StringAttribute{ - DeprecationMessage: deprecationMsg, - Description: "Classless Inter-Domain Routing (CIDR) for configuration of network area for region `eu01`.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - "default_prefix_length": schema.Int64Attribute{ - DeprecationMessage: deprecationMsg, - Description: "The default prefix length for networks in the network area for region `eu01`.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(24), - int64validator.AtMost(29), - }, - Default: int64default.StaticInt64(defaultValueDefaultPrefixLength), - }, - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - "max_prefix_length": schema.Int64Attribute{ - DeprecationMessage: deprecationMsg, - Description: "The maximal prefix length for networks in the network area for region `eu01`.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(24), - int64validator.AtMost(29), - }, - Default: int64default.StaticInt64(defaultValueMaxPrefixLength), - }, - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - "min_prefix_length": schema.Int64Attribute{ - DeprecationMessage: deprecationMsg, - Description: "The minimal prefix length for networks in the network area for region `eu01`.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(8), - int64validator.AtMost(29), - }, - Default: int64default.StaticInt64(defaultValueMinPrefixLength), - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network area - networkArea, err := r.client.CreateNetworkArea(ctx, organizationId).CreateNetworkAreaPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - networkAreaId := *networkArea.Id - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - // Map response body to schema - err = mapFields(ctx, networkArea, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - if model.LegacyMode() { - core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails) - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - regionCreatePayload, err := toRegionCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - networkAreaRegionCreateResp, err := r.client.CreateNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").CreateNetworkAreaRegionPayload(*regionCreatePayload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - err = mapNetworkAreaRegionFields(ctx, networkAreaRegionCreateResp, &model) // map partial state - just in case anything goes wrong during the wait handler - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - networkAreaRegionResp, err := wait.CreateNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, "eu01").WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for network area region creation", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - } else { - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) - model.DefaultNameservers = types.ListNull(types.StringType) - model.TransferNetwork = types.StringNull() - model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) - model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) - model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) - } - - // Set state to fully populated data - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - resp.Diagnostics.Append(req.State.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - networkAreaResp, err := r.client.GetNetworkArea(ctx, organizationId, networkAreaId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, networkAreaResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - if model.LegacyMode() { - core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails) - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if !(ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest)) { // TODO: iaas api returns http 400 in case network area region is not found - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) - model.DefaultNameservers = types.ListNull(types.StringType) - model.TransferNetwork = types.StringNull() - model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) - model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) - model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) - } else { - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - } - } else { - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) - model.DefaultNameservers = types.ListNull(types.StringType) - model.TransferNetwork = types.StringNull() - model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) - model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) - model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) - } - - // Set refreshed state - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - ranges := []networkRange{} - if !(model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown()) { - resp.Diagnostics.Append(model.NetworkRanges.ElementsAs(ctx, &ranges, false)...) - if resp.Diagnostics.HasError() { - return - } - } - - // Retrieve values from state - var stateModel Model - resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing network - networkAreaUpdateResp, err := r.client.PartialUpdateNetworkArea(ctx, organizationId, networkAreaId).PartialUpdateNetworkAreaPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, networkAreaUpdateResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - if model.LegacyMode() { - core.LogAndAddWarning(ctx, &resp.Diagnostics, deprecationWarningSummary, deprecationWarningDetails) - - // Deprecated: Update network area region payload creation. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - regionUpdatePayload, err := toRegionUpdatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Deprecated: Update network area region. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - networkAreaRegionUpdateResp, err := r.client.UpdateNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").UpdateNetworkAreaRegionPayload(*regionUpdatePayload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Deprecated: Update network area region. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - err = mapNetworkAreaRegionFields(ctx, networkAreaRegionUpdateResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Deprecated: Update network ranges. Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - err = updateNetworkRanges(ctx, organizationId, networkAreaId, ranges, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Updating Network ranges: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, "eu01").Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusBadRequest) { // TODO: iaas api returns http 400 in case network area region is not found - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - err = mapNetworkAreaRegionFields(ctx, networkAreaRegionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - } else { - // Deprecated: Will be removed in May 2026. Only introduced to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) - model.DefaultNameservers = types.ListNull(types.StringType) - model.TransferNetwork = types.StringNull() - model.DefaultPrefixLength = types.Int64Value(defaultValueDefaultPrefixLength) - model.MinPrefixLength = types.Int64Value(defaultValueMinPrefixLength) - model.MaxPrefixLength = types.Int64Value(defaultValueMaxPrefixLength) - } - - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - _, err := wait.ReadyForNetworkAreaDeletionWaitHandler(ctx, r.client, r.resourceManagerClient, organizationId, networkAreaId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Network area ready for deletion waiting: %v", err)) - return - } - - // Get all configured regions so we can delete them one by one before deleting the network area - regionsListResp, err := r.client.ListNetworkAreaRegions(ctx, organizationId, networkAreaId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API to list configured regions: %v", err)) - return - } - - // Delete network region configurations - for region := range *regionsListResp.Regions { - err = r.client.DeleteNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - _, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Waiting for networea deletion: %v", err)) - return - } - } - - // Delete existing network area - err = r.client.DeleteNetworkArea(ctx, organizationId, networkAreaId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Network area deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id -func (r *networkAreaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network area", - fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id] Got: %q", req.ID), - ) - return - } - - organizationId := idParts[0] - networkAreaId := idParts[1] - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_id"), networkAreaId)...) - tflog.Info(ctx, "Network state imported") -} - -func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, model *Model) error { - if networkAreaResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkAreaId string - if model.NetworkAreaId.ValueString() != "" { - networkAreaId = model.NetworkAreaId.ValueString() - } else if networkAreaResp.Id != nil { - networkAreaId = *networkAreaResp.Id - } else { - return fmt.Errorf("network area id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), networkAreaId) - - labels, err := iaasUtils.MapLabels(ctx, networkAreaResp.Labels, model.Labels) - if err != nil { - return err - } - - model.NetworkAreaId = types.StringValue(networkAreaId) - model.Name = types.StringPointerValue(networkAreaResp.Name) - model.ProjectCount = types.Int64PointerValue(networkAreaResp.ProjectCount) - model.Labels = labels - - return nil -} - -// Deprecated: mapRegionFields maps the region configuration for eu01 to avoid a breaking change in the Terraform provider during the IaaS v1 -> v2 API migration. Will be removed in May 2026. -func mapNetworkAreaRegionFields(ctx context.Context, networkAreaRegionResp *iaas.RegionalArea, model *Model) error { - if model == nil { - return fmt.Errorf("model input is nil") - } - if networkAreaRegionResp == nil { - return fmt.Errorf("response input is nil") - } - - // map default nameservers - if networkAreaRegionResp.Ipv4 == nil || networkAreaRegionResp.Ipv4.DefaultNameservers == nil { - model.DefaultNameservers = types.ListNull(types.StringType) - } else { - respDefaultNameservers := *networkAreaRegionResp.Ipv4.DefaultNameservers - modelDefaultNameservers, err := utils.ListValuetoStringSlice(model.DefaultNameservers) - if err != nil { - return fmt.Errorf("get current network area default nameservers from model: %w", err) - } - - reconciledDefaultNameservers := utils.ReconcileStringSlices(modelDefaultNameservers, respDefaultNameservers) - - defaultNameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledDefaultNameservers) - if diags.HasError() { - return fmt.Errorf("map network area default nameservers: %w", core.DiagsToError(diags)) - } - - model.DefaultNameservers = defaultNameserversTF - } - - // map network ranges - if networkAreaRegionResp.Ipv4 == nil || networkAreaRegionResp.Ipv4.NetworkRanges == nil { - model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) - } else { - err := mapNetworkRanges(ctx, networkAreaRegionResp.Ipv4.NetworkRanges, model) - if err != nil { - return fmt.Errorf("mapping network ranges: %w", err) - } - } - - // map remaining fields - if networkAreaRegionResp.Ipv4 != nil { - model.TransferNetwork = types.StringPointerValue(networkAreaRegionResp.Ipv4.TransferNetwork) - model.DefaultPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.DefaultPrefixLen) - model.MaxPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.MaxPrefixLen) - model.MinPrefixLength = types.Int64PointerValue(networkAreaRegionResp.Ipv4.MinPrefixLen) - } - - return nil -} - -// Deprecated: mapNetworkRanges will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only kept to circumvent breaking changes. -func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.NetworkRange, model *Model) error { - var diags diag.Diagnostics - - if networkAreaRangesList == nil { - return fmt.Errorf("nil network area ranges list") - } - if len(*networkAreaRangesList) == 0 { - model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) - return nil - } - - ranges := []networkRange{} - if !(model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown()) { - diags = model.NetworkRanges.ElementsAs(ctx, &ranges, false) - if diags.HasError() { - return fmt.Errorf("map network ranges: %w", core.DiagsToError(diags)) - } - } - - modelNetworkRangePrefixes := []string{} - for _, m := range ranges { - modelNetworkRangePrefixes = append(modelNetworkRangePrefixes, m.Prefix.ValueString()) - } - - apiNetworkRangePrefixes := []string{} - for _, n := range *networkAreaRangesList { - apiNetworkRangePrefixes = append(apiNetworkRangePrefixes, *n.Prefix) - } - - reconciledRangePrefixes := utils.ReconcileStringSlices(modelNetworkRangePrefixes, apiNetworkRangePrefixes) - - networkRangesList := []attr.Value{} - for i, prefix := range reconciledRangePrefixes { - var networkRangeId string - for _, networkRangeElement := range *networkAreaRangesList { - if *networkRangeElement.Prefix == prefix { - networkRangeId = *networkRangeElement.Id - break - } - } - networkRangeMap := map[string]attr.Value{ - "prefix": types.StringValue(prefix), - "network_range_id": types.StringValue(networkRangeId), - } - - networkRangeTF, diags := types.ObjectValue(networkRangeTypes, networkRangeMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - - networkRangesList = append(networkRangesList, networkRangeTF) - } - - networkRangesTF, diags := types.ListValue( - types.ObjectType{AttrTypes: networkRangeTypes}, - networkRangesList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - model.NetworkRanges = networkRangesTF - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.CreateNetworkAreaPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - }, nil -} - -// Deprecated: toRegionCreatePayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. -func toRegionCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRegionPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting default nameservers: %w", err) - } - - networkRangesPayload, err := toNetworkRangesPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting network ranges: %w", err) - } - - return &iaas.CreateNetworkAreaRegionPayload{ - Ipv4: &iaas.RegionalAreaIPv4{ - DefaultNameservers: &modelDefaultNameservers, - DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength), - MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength), - MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength), - TransferNetwork: conversion.StringValueToPointer(model.TransferNetwork), - NetworkRanges: networkRangesPayload, - }, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.PartialUpdateNetworkAreaPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.PartialUpdateNetworkAreaPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - }, nil -} - -// Deprecated: toRegionUpdatePayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. -func toRegionUpdatePayload(ctx context.Context, model *Model) (*iaas.UpdateNetworkAreaRegionPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting default nameservers: %w", err) - } - - return &iaas.UpdateNetworkAreaRegionPayload{ - Ipv4: &iaas.UpdateRegionalAreaIPv4{ - DefaultNameservers: &modelDefaultNameservers, - DefaultPrefixLen: conversion.Int64ValueToPointer(model.DefaultPrefixLength), - MaxPrefixLen: conversion.Int64ValueToPointer(model.MaxPrefixLength), - MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength), - }, - }, nil -} - -// Deprecated: toDefaultNameserversPayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. -func toDefaultNameserversPayload(_ context.Context, model *Model) ([]string, error) { - modelDefaultNameservers := []string{} - for _, ns := range model.DefaultNameservers.Elements() { - nameserverString, ok := ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString()) - } - - return modelDefaultNameservers, nil -} - -// Deprecated: toNetworkRangesPayload will be removed in May 2026. Implementation won't be needed anymore because of the IaaS API v1 -> v2 migration. Func was only introduced to circumvent breaking changes. -func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkRange, error) { - if model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown() { - return nil, nil - } - - networkRangesModel := []networkRange{} - diags := model.NetworkRanges.ElementsAs(ctx, &networkRangesModel, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - if len(networkRangesModel) == 0 { - return nil, nil - } - - payload := []iaas.NetworkRange{} - for i := range networkRangesModel { - networkRangeModel := networkRangesModel[i] - payload = append(payload, iaas.NetworkRange{ - Prefix: conversion.StringValueToPointer(networkRangeModel.Prefix), - }) - } - - return &payload, nil -} - -// Deprecated: updateNetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model. This was only kept to make the v1 -> v2 IaaS API migration non-breaking in the Terraform provider. -func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId string, ranges []networkRange, client *iaas.APIClient) error { - // Get network ranges current state - currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId, "eu01").Execute() - if err != nil { - return fmt.Errorf("error reading network area ranges: %w", err) - } - - type networkRangeState struct { - isInModel bool - isCreated bool - id string - } - - networkRangesState := make(map[string]*networkRangeState) - for _, nwRange := range ranges { - networkRangesState[nwRange.Prefix.ValueString()] = &networkRangeState{ - isInModel: true, - } - } - - for _, networkRange := range *currentNetworkRangesResp.Items { - prefix := *networkRange.Prefix - if _, ok := networkRangesState[prefix]; !ok { - networkRangesState[prefix] = &networkRangeState{} - } - networkRangesState[prefix].isCreated = true - networkRangesState[prefix].id = *networkRange.Id - } - - // Delete network ranges - for prefix, state := range networkRangesState { - if !state.isInModel && state.isCreated { - err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, "eu01", state.id).Execute() - if err != nil { - return fmt.Errorf("deleting network area range '%v': %w", prefix, err) - } - } - } - - // Create network ranges - for prefix, state := range networkRangesState { - if state.isInModel && !state.isCreated { - payload := iaas.CreateNetworkAreaRangePayload{ - Ipv4: &[]iaas.NetworkRange{ - { - Prefix: sdkUtils.Ptr(prefix), - }, - }, - } - - _, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId, "eu01").CreateNetworkAreaRangePayload(payload).Execute() - if err != nil { - return fmt.Errorf("creating network range '%v': %w", prefix, err) - } - } - } - - return nil -} diff --git a/stackit/internal/services/iaas/networkarea/resource_test.go b/stackit/internal/services/iaas/networkarea/resource_test.go deleted file mode 100644 index dbcdfbb5..00000000 --- a/stackit/internal/services/iaas/networkarea/resource_test.go +++ /dev/null @@ -1,1123 +0,0 @@ -package networkarea - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/gorilla/mux" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -var testOrganizationId = uuid.NewString() -var testAreaId = uuid.NewString() -var testRangeId1 = uuid.NewString() -var testRangeId2 = uuid.NewString() -var testRangeId3 = uuid.NewString() -var testRangeId4 = uuid.NewString() -var testRangeId5 = uuid.NewString() -var testRangeId2Repeated = uuid.NewString() - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *iaas.NetworkArea - expected Model - isValid bool - }{ - { - description: "id_ok", - state: Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - DefaultNameservers: types.ListNull(types.StringType), - }, - input: &iaas.NetworkArea{ - Id: utils.Ptr("naid"), - }, - expected: Model{ - Id: types.StringValue("oid,naid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - Name: types.StringNull(), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - DefaultNameservers: types.ListNull(types.StringType), - Labels: types.MapNull(types.StringType), - }, - isValid: true, - }, - { - description: "values_ok", - state: Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - DefaultNameservers: types.ListNull(types.StringType), - }, - input: &iaas.NetworkArea{ - Id: utils.Ptr("naid"), - Name: utils.Ptr("name"), - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - expected: Model{ - Id: types.StringValue("oid,naid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - Name: types.StringValue("name"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - DefaultNameservers: types.ListNull(types.StringType), - }, - isValid: true, - }, - { - description: "default_nameservers_changed_outside_tf", - state: Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - DefaultNameservers: types.ListNull(types.StringType), - }, - input: &iaas.NetworkArea{ - Id: utils.Ptr("naid"), - }, - expected: Model{ - Id: types.StringValue("oid,naid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - Labels: types.MapNull(types.StringType), - DefaultNameservers: types.ListNull(types.StringType), - }, - isValid: true, - }, - { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - OrganizationId: types.StringValue("oid"), - }, - &iaas.NetworkArea{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -// Deprecated: Will be removed in May 2026. -func Test_MapNetworkRanges(t *testing.T) { - type args struct { - networkAreaRangesList *[]iaas.NetworkRange - model *Model - } - tests := []struct { - name string - args args - want *Model - wantErr bool - }{ - { - name: "model and response have ranges in different order", - args: args{ - model: &Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - DefaultNameservers: types.ListNull(types.StringType), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - Labels: types.MapNull(types.StringType), - }, - networkAreaRangesList: &[]iaas.NetworkRange{ - { - Id: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - { - Id: utils.Ptr(testRangeId3), - Prefix: utils.Ptr("prefix-3"), - }, - { - Id: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - }, - }, - want: &Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId3), - "prefix": types.StringValue("prefix-3"), - }), - }), - Labels: types.MapNull(types.StringType), - DefaultNameservers: types.ListNull(types.StringType), - }, - wantErr: false, - }, - { - name: "network_ranges_changed_outside_tf", - args: args{ - model: &Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - Labels: types.MapNull(types.StringType), - DefaultNameservers: types.ListNull(types.StringType), - }, - networkAreaRangesList: &[]iaas.NetworkRange{ - { - Id: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - { - Id: utils.Ptr(testRangeId3), - Prefix: utils.Ptr("prefix-3"), - }, - }, - }, - want: &Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId3), - "prefix": types.StringValue("prefix-3"), - }), - }), - Labels: types.MapNull(types.StringType), - DefaultNameservers: types.ListNull(types.StringType), - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := mapNetworkRanges(context.Background(), tt.args.networkAreaRangesList, tt.args.model); (err != nil) != tt.wantErr { - t.Errorf("mapNetworkRanges() error = %v, wantErr %v", err, tt.wantErr) - } - - diff := cmp.Diff(tt.args.model, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -// Deprecated: Will be removed in May 2026. -func TestMapNetworkAreaRegionFields(t *testing.T) { - type args struct { - networkAreaRegionResp *iaas.RegionalArea - model *Model - } - tests := []struct { - name string - args args - want *Model - wantErr bool - }{ - { - name: "default", - args: args{ - model: &Model{ - Labels: types.MapNull(types.StringType), - }, - networkAreaRegionResp: &iaas.RegionalArea{ - Ipv4: &iaas.RegionalAreaIPv4{ - DefaultNameservers: &[]string{ - "nameserver1", - "nameserver2", - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - NetworkRanges: &[]iaas.NetworkRange{ - { - Id: utils.Ptr(testRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - { - Id: utils.Ptr(testRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - }, - }, - }, - }, - want: &Model{ - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nameserver1"), - types.StringValue("nameserver2"), - }), - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId1), - "prefix": types.StringValue("prefix-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringValue(testRangeId2), - "prefix": types.StringValue("prefix-2"), - }), - }), - - Labels: types.MapNull(types.StringType), - }, - wantErr: false, - }, - { - name: "model is nil", - args: args{ - model: nil, - networkAreaRegionResp: &iaas.RegionalArea{}, - }, - want: nil, - wantErr: true, - }, - { - name: "network area region response is nil", - args: args{ - model: &Model{ - DefaultNameservers: types.ListNull(types.StringType), - NetworkRanges: types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}), - Labels: types.MapNull(types.StringType), - }, - networkAreaRegionResp: nil, - }, - want: &Model{ - DefaultNameservers: types.ListNull(types.StringType), - NetworkRanges: types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}), - Labels: types.MapNull(types.StringType), - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := mapNetworkAreaRegionFields(context.Background(), tt.args.networkAreaRegionResp, tt.args.model); (err != nil) != tt.wantErr { - t.Errorf("mapNetworkAreaRegionFields() error = %v, wantErr %v", err, tt.wantErr) - } - - diff := cmp.Diff(tt.args.model, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateNetworkAreaPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - &iaas.CreateNetworkAreaPayload{ - Name: utils.Ptr("name"), - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -// Deprecated: Will be removed in May 2026. -func TestToRegionCreatePayload(t *testing.T) { - type args struct { - model *Model - } - tests := []struct { - name string - args args - want *iaas.CreateNetworkAreaRegionPayload - wantErr bool - }{ - { - name: "default_ok", - args: args{ - model: &Model{ - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - NetworkRanges: types.ListValueMust(types.ObjectType{AttrTypes: networkRangeTypes}, []attr.Value{ - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringUnknown(), - "prefix": types.StringValue("pr-1"), - }), - types.ObjectValueMust(networkRangeTypes, map[string]attr.Value{ - "network_range_id": types.StringUnknown(), - "prefix": types.StringValue("pr-2"), - }), - }), - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), - }, - }, - want: &iaas.CreateNetworkAreaRegionPayload{ - Ipv4: &iaas.RegionalAreaIPv4{ - DefaultNameservers: &[]string{ - "ns1", - "ns2", - }, - NetworkRanges: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("pr-1"), - }, - { - Prefix: utils.Ptr("pr-2"), - }, - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - }, - }, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toRegionCreatePayload(context.Background(), tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toRegionCreatePayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - diff := cmp.Diff(got, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.PartialUpdateNetworkAreaPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - &iaas.PartialUpdateNetworkAreaPayload{ - Name: utils.Ptr("name"), - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -// Deprecated: Will be removed in May 2026. -func TestToRegionUpdatePayload(t *testing.T) { - type args struct { - model *Model - } - tests := []struct { - name string - args args - want *iaas.UpdateNetworkAreaRegionPayload - wantErr bool - }{ - { - name: "default_ok", - args: args{ - model: &Model{ - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - DefaultPrefixLength: types.Int64Value(22), - MaxPrefixLength: types.Int64Value(24), - MinPrefixLength: types.Int64Value(20), - }, - }, - want: &iaas.UpdateNetworkAreaRegionPayload{ - Ipv4: &iaas.UpdateRegionalAreaIPv4{ - DefaultNameservers: &[]string{ - "ns1", - "ns2", - }, - DefaultPrefixLen: utils.Ptr(int64(22)), - MaxPrefixLen: utils.Ptr(int64(24)), - MinPrefixLen: utils.Ptr(int64(20)), - }, - }, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toRegionUpdatePayload(context.Background(), tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toRegionUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - diff := cmp.Diff(got, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func TestUpdateNetworkRanges(t *testing.T) { - getAllNetworkRangesResp := iaas.NetworkRangeListResponse{ - Items: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("pr-1"), - Id: utils.Ptr(testRangeId1), - }, - { - Prefix: utils.Ptr("pr-2"), - Id: utils.Ptr(testRangeId2), - }, - { - Prefix: utils.Ptr("pr-3"), - Id: utils.Ptr(testRangeId3), - }, - { - Prefix: utils.Ptr("pr-2"), - Id: utils.Ptr(testRangeId2Repeated), - }, - }, - } - getAllNetworkRangesRespBytes, err := json.Marshal(getAllNetworkRangesResp) - if err != nil { - t.Fatalf("Failed to marshal get all network ranges response: %v", err) - } - - // This is the response used whenever an API returns a failure response - failureRespBytes := []byte("{\"message\": \"Something bad happened\"") - - tests := []struct { - description string - networkRanges []networkRange - ipv4 []iaas.NetworkRange - getAllNetworkRangesFails bool - createNetworkRangesFails bool - deleteNetworkRangesFails bool - isValid bool - expectedNetworkRangesStates map[string]bool // Keys are prefix; value is true if prefix should exist at the end, false if should be deleted - }{ - { - description: "no_changes", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": true, - }, - isValid: true, - }, - { - description: "create_network_ranges", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(testRangeId4), - Prefix: types.StringValue("pr-4"), - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": true, - "pr-4": true, - }, - isValid: true, - }, - { - description: "delete_network_ranges", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": false, - "pr-3": true, - }, - isValid: true, - }, - { - description: "multiple_changes", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(testRangeId4), - Prefix: types.StringValue("pr-4"), - }, - { - NetworkRangeId: types.StringValue(testRangeId5), - Prefix: types.StringValue("pr-5"), - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": false, - "pr-3": true, - "pr-4": true, - "pr-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_repetition", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(testRangeId4), - Prefix: types.StringValue("pr-4"), - }, - { - NetworkRangeId: types.StringValue(testRangeId5), - Prefix: types.StringValue("pr-5"), - }, - { - NetworkRangeId: types.StringValue(testRangeId5), - Prefix: types.StringValue("pr-5"), - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": false, - "pr-3": true, - "pr-4": true, - "pr-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_2", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId4), - Prefix: types.StringValue("pr-4"), - }, - { - NetworkRangeId: types.StringValue(testRangeId5), - Prefix: types.StringValue("pr-5"), - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": false, - "pr-2": false, - "pr-3": false, - "pr-4": true, - "pr-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_3", - networkRanges: []networkRange{}, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": false, - "pr-2": false, - "pr-3": false, - }, - isValid: true, - }, - { - description: "get_fails", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - }, - getAllNetworkRangesFails: true, - isValid: false, - }, - { - description: "create_fails_1", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(testRangeId4), - Prefix: types.StringValue("pr-4"), - }, - }, - createNetworkRangesFails: true, - isValid: false, - }, - { - description: "create_fails_2", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId2), - Prefix: types.StringValue("pr-2"), - }, - }, - createNetworkRangesFails: true, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": false, - }, - isValid: true, - }, - { - description: "delete_fails_1", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId2), - Prefix: types.StringValue("pr-2"), - }, - }, - deleteNetworkRangesFails: true, - isValid: false, - }, - { - description: "delete_fails_2", - networkRanges: []networkRange{ - { - NetworkRangeId: types.StringValue(testRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(testRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(testRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(testRangeId4), - Prefix: types.StringValue("pr-4"), - }, - }, - deleteNetworkRangesFails: true, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": true, - "pr-4": true, - }, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - // Will be compared to tt.expectedNetworkRangesStates at the end - networkRangesStates := make(map[string]bool) - networkRangesStates["pr-1"] = true - networkRangesStates["pr-2"] = true - networkRangesStates["pr-3"] = true - - // Handler for getting all network ranges - getAllNetworkRangesHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - if tt.getAllNetworkRangesFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Get all network ranges handler: failed to write bad response: %v", err) - } - return - } - - _, err := w.Write(getAllNetworkRangesRespBytes) - if err != nil { - t.Errorf("Get all network ranges handler: failed to write response: %v", err) - } - }) - - // Handler for creating network range - createNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - var payload iaas.CreateNetworkAreaRangePayload - err := decoder.Decode(&payload) - if err != nil { - t.Errorf("Create network range handler: failed to parse payload") - return - } - if payload.Ipv4 == nil { - t.Errorf("Create network range handler: nil Ipv4") - return - } - ipv4 := *payload.Ipv4 - - for _, networkRange := range ipv4 { - prefix := *networkRange.Prefix - if prefixExists, prefixWasCreated := networkRangesStates[prefix]; prefixWasCreated && prefixExists { - t.Errorf("Create network range handler: attempted to create range '%v' that already exists", *payload.Ipv4) - return - } - w.Header().Set("Content-Type", "application/json") - if tt.createNetworkRangesFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Create network ranges handler: failed to write bad response: %v", err) - } - return - } - - resp := iaas.NetworkRange{ - Prefix: utils.Ptr("prefix"), - Id: utils.Ptr("id-range"), - } - respBytes, err := json.Marshal(resp) - if err != nil { - t.Errorf("Create network range handler: failed to marshal response: %v", err) - return - } - _, err = w.Write(respBytes) - if err != nil { - t.Errorf("Create network range handler: failed to write response: %v", err) - } - networkRangesStates[prefix] = true - } - }) - - // Handler for deleting Network range - deleteNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - networkRangeId, ok := vars["networkRangeId"] - if !ok { - t.Errorf("Delete network range handler: no range ID") - return - } - - var prefix string - for _, rangeItem := range *getAllNetworkRangesResp.Items { - if *rangeItem.Id == networkRangeId { - prefix = *rangeItem.Prefix - } - } - prefixExists, prefixWasCreated := networkRangesStates[prefix] - if !prefixWasCreated { - t.Errorf("Delete network range handler: attempted to delete range '%v' that wasn't created", prefix) - return - } - if prefixWasCreated && !prefixExists { - t.Errorf("Delete network range handler: attempted to delete range '%v' that was already deleted", prefix) - return - } - - w.Header().Set("Content-Type", "application/json") - if tt.deleteNetworkRangesFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Delete network range handler: failed to write bad response: %v", err) - } - return - } - - _, err = w.Write([]byte("{}")) - if err != nil { - t.Errorf("Delete network range handler: failed to write response: %v", err) - } - networkRangesStates[prefix] = false - }) - - // Setup server and client - router := mux.NewRouter() - router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - getAllNetworkRangesHandler(w, r) - } else if r.Method == "POST" { - createNetworkRangeHandler(w, r) - } - }) - router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler) - mockedServer := httptest.NewServer(router) - defer mockedServer.Close() - client, err := iaas.NewAPIClient( - config.WithEndpoint(mockedServer.URL), - config.WithoutAuthentication(), - ) - if err != nil { - t.Fatalf("Failed to initialize client: %v", err) - } - - // Run test - err = updateNetworkRanges(context.Background(), testOrganizationId, testAreaId, tt.networkRanges, client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(networkRangesStates, tt.expectedNetworkRangesStates) - if diff != "" { - t.Fatalf("Network range states do not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/networkarearegion/datasource.go b/stackit/internal/services/iaas/networkarearegion/datasource.go deleted file mode 100644 index efa9648a..00000000 --- a/stackit/internal/services/iaas/networkarearegion/datasource.go +++ /dev/null @@ -1,181 +0,0 @@ -package networkarearegion - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &networkAreaRegionDataSource{} -) - -// NewNetworkAreaRegionDataSource is a helper function to simplify the provider implementation. -func NewNetworkAreaRegionDataSource() datasource.DataSource { - return &networkAreaRegionDataSource{} -} - -// networkAreaRegionDataSource is the data source implementation. -type networkAreaRegionDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *networkAreaRegionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_area_region" -} - -func (d *networkAreaRegionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (d *networkAreaRegionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Network area region data source schema." - - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`\".", - Computed: true, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the network area is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "ipv4": schema.SingleNestedAttribute{ - Computed: true, - Description: "The regional IPv4 config of a network area.", - Attributes: map[string]schema.Attribute{ - "default_nameservers": schema.ListAttribute{ - Description: "List of DNS Servers/Nameservers.", - Computed: true, - ElementType: types.StringType, - }, - "network_ranges": schema.ListNestedAttribute{ - Description: "List of Network ranges.", - Computed: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - listvalidator.SizeAtMost(64), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "network_range_id": schema.StringAttribute{ - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "prefix": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Computed: true, - }, - }, - }, - }, - "transfer_network": schema.StringAttribute{ - Description: "IPv4 Classless Inter-Domain Routing (CIDR).", - Computed: true, - }, - "default_prefix_length": schema.Int64Attribute{ - Description: "The default prefix length for networks in the network area.", - Computed: true, - }, - "max_prefix_length": schema.Int64Attribute{ - Description: "The maximal prefix length for networks in the network area.", - Computed: true, - }, - "min_prefix_length": schema.Int64Attribute{ - Description: "The minimal prefix length for networks in the network area.", - Computed: true, - }, - }, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *networkAreaRegionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - - networkAreaRegionResp, err := d.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() - if err != nil { - utils.LogError(ctx, &resp.Diagnostics, err, "Reading network area region", fmt.Sprintf("Region configuration for %q for network area %q does not exist.", region, networkAreaId), nil) - resp.State.RemoveResource(ctx) - return - } - - // Map response body to schema - err = mapFields(ctx, networkAreaRegionResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area region read") -} diff --git a/stackit/internal/services/iaas/networkarearegion/resource.go b/stackit/internal/services/iaas/networkarearegion/resource.go deleted file mode 100644 index 36dd3a1a..00000000 --- a/stackit/internal/services/iaas/networkarearegion/resource.go +++ /dev/null @@ -1,728 +0,0 @@ -package networkarearegion - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &networkAreaRegionResource{} - _ resource.ResourceWithConfigure = &networkAreaRegionResource{} - _ resource.ResourceWithImportState = &networkAreaRegionResource{} - _ resource.ResourceWithModifyPlan = &networkAreaRegionResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Region types.String `tfsdk:"region"` - Ipv4 *ipv4Model `tfsdk:"ipv4"` -} - -// Struct corresponding to Model.Ipv4 -type ipv4Model struct { - DefaultNameservers types.List `tfsdk:"default_nameservers"` - NetworkRanges []networkRangeModel `tfsdk:"network_ranges"` - TransferNetwork types.String `tfsdk:"transfer_network"` - DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"` - MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"` - MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"` -} - -// Struct corresponding to Model.NetworkRanges[i] -type networkRangeModel struct { - Prefix types.String `tfsdk:"prefix"` - NetworkRangeId types.String `tfsdk:"network_range_id"` -} - -// NewNetworkAreaRegionResource is a helper function to simplify the provider implementation. -func NewNetworkAreaRegionResource() resource.Resource { - return &networkAreaRegionResource{} -} - -// networkAreaRegionResource is the resource implementation. -type networkAreaRegionResource struct { - client *iaas.APIClient - resourceManagerClient *resourcemanager.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *networkAreaRegionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_area_region" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *networkAreaRegionResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *networkAreaRegionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - r.client = iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - r.resourceManagerClient = resourcemanagerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *networkAreaRegionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network area region resource schema." - - resp.Schema = schema.Schema{ - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the network area is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "ipv4": schema.SingleNestedAttribute{ - Description: "The regional IPv4 config of a network area.", - Required: true, - Attributes: map[string]schema.Attribute{ - "default_nameservers": schema.ListAttribute{ - Description: "List of DNS Servers/Nameservers.", - Optional: true, - ElementType: types.StringType, - }, - "network_ranges": schema.ListNestedAttribute{ - Description: "List of Network ranges.", - Required: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - listvalidator.SizeAtMost(64), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "network_range_id": schema.StringAttribute{ - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "prefix": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR).", - Required: true, - }, - }, - }, - }, - "transfer_network": schema.StringAttribute{ - Description: "IPv4 Classless Inter-Domain Routing (CIDR).", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "default_prefix_length": schema.Int64Attribute{ - Description: "The default prefix length for networks in the network area.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(24), - int64validator.AtMost(29), - }, - Default: int64default.StaticInt64(25), - }, - "max_prefix_length": schema.Int64Attribute{ - Description: "The maximal prefix length for networks in the network area.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(24), - int64validator.AtMost(29), - }, - Default: int64default.StaticInt64(29), - }, - "min_prefix_length": schema.Int64Attribute{ - Description: "The minimal prefix length for networks in the network area.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.AtLeast(8), - int64validator.AtMost(29), - }, - Default: int64default.StaticInt64(24), - }, - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *networkAreaRegionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - - ctx = core.InitProviderContext(ctx) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network area region configuration - networkAreaRegion, err := r.client.CreateNetworkAreaRegion(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRegionPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "organization_id": organizationId, - "network_area_id": networkAreaId, - "region": region, - }) - - // wait for creation of network area region to complete - _, err = wait.CreateNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, networkAreaRegion, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area region created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *networkAreaRegionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - resp.Diagnostics.Append(req.State.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - - ctx = core.InitProviderContext(ctx) - - networkAreaRegionResp, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, networkAreaRegionResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area region read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *networkAreaRegionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - - ctx = core.InitProviderContext(ctx) - - // Retrieve values from state - var stateModel Model - resp.Diagnostics.Append(req.State.Get(ctx, &stateModel)...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Update existing network area region configuration - _, err = r.client.UpdateNetworkAreaRegion(ctx, organizationId, networkAreaId, region).UpdateNetworkAreaRegionPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = updateIpv4NetworkRanges(ctx, organizationId, networkAreaId, model.Ipv4.NetworkRanges, r.client, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Updating Network ranges: %v", err)) - return - } - - updatedNetworkAreaRegion, err := r.client.GetNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - err = mapFields(ctx, updatedNetworkAreaRegion, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area region", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "network area region updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *networkAreaRegionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - resp.Diagnostics.Append(req.State.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - - _, err := wait.ReadyForNetworkAreaDeletionWaitHandler(ctx, r.client, r.resourceManagerClient, organizationId, networkAreaId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Network area ready for deletion waiting: %v", err)) - return - } - - ctx = core.InitProviderContext(ctx) - - // Delete network area region configuration - err = r.client.DeleteNetworkAreaRegion(ctx, organizationId, networkAreaId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, r.client, organizationId, networkAreaId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area region", fmt.Sprintf("network area deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Network area region deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: organization_id,network_area_id,region -func (r *networkAreaRegionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network area region", - fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[region] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "organization_id": idParts[0], - "network_area_id": idParts[1], - "region": idParts[2], - }) - - tflog.Info(ctx, "Network area region state imported") -} - -// mapFields maps the API response values to the Terraform resource model fields -func mapFields(ctx context.Context, networkAreaRegion *iaas.RegionalArea, model *Model, region string) error { - if networkAreaRegion == nil { - return fmt.Errorf("network are region input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), region) - model.Region = types.StringValue(region) - - model.Ipv4 = &ipv4Model{} - if networkAreaRegion.Ipv4 != nil { - model.Ipv4.TransferNetwork = types.StringPointerValue(networkAreaRegion.Ipv4.TransferNetwork) - model.Ipv4.DefaultPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.DefaultPrefixLen) - model.Ipv4.MaxPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.MaxPrefixLen) - model.Ipv4.MinPrefixLength = types.Int64PointerValue(networkAreaRegion.Ipv4.MinPrefixLen) - } - - // map default nameservers - if networkAreaRegion.Ipv4 == nil || networkAreaRegion.Ipv4.DefaultNameservers == nil { - model.Ipv4.DefaultNameservers = types.ListNull(types.StringType) - } else { - respDefaultNameservers := *networkAreaRegion.Ipv4.DefaultNameservers - modelDefaultNameservers, err := utils.ListValuetoStringSlice(model.Ipv4.DefaultNameservers) - if err != nil { - return fmt.Errorf("get current network area default nameservers from model: %w", err) - } - - reconciledDefaultNameservers := utils.ReconcileStringSlices(modelDefaultNameservers, respDefaultNameservers) - - defaultNameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledDefaultNameservers) - if diags.HasError() { - return fmt.Errorf("map network area default nameservers: %w", core.DiagsToError(diags)) - } - - model.Ipv4.DefaultNameservers = defaultNameserversTF - } - - // map network ranges - err := mapIpv4NetworkRanges(ctx, networkAreaRegion.Ipv4.NetworkRanges, model) - if err != nil { - return fmt.Errorf("mapping network ranges: %w", err) - } - - return nil -} - -// mapFields maps the API ipv4 network ranges response values to the Terraform resource model fields -func mapIpv4NetworkRanges(_ context.Context, networkAreaRangesList *[]iaas.NetworkRange, model *Model) error { - if networkAreaRangesList == nil { - return fmt.Errorf("nil network area ranges list") - } - if len(*networkAreaRangesList) == 0 { - model.Ipv4.NetworkRanges = []networkRangeModel{} - return nil - } - - modelNetworkRangePrefixes := []string{} - for _, m := range model.Ipv4.NetworkRanges { - modelNetworkRangePrefixes = append(modelNetworkRangePrefixes, m.Prefix.ValueString()) - } - - apiNetworkRangePrefixes := []string{} - for _, n := range *networkAreaRangesList { - apiNetworkRangePrefixes = append(apiNetworkRangePrefixes, *n.Prefix) - } - - reconciledRangePrefixes := utils.ReconcileStringSlices(modelNetworkRangePrefixes, apiNetworkRangePrefixes) - - model.Ipv4.NetworkRanges = []networkRangeModel{} - for _, prefix := range reconciledRangePrefixes { - var networkRangeId string - for _, networkRangeElement := range *networkAreaRangesList { - if *networkRangeElement.Prefix == prefix { - networkRangeId = *networkRangeElement.Id - break - } - } - - model.Ipv4.NetworkRanges = append(model.Ipv4.NetworkRanges, networkRangeModel{ - Prefix: types.StringValue(prefix), - NetworkRangeId: types.StringValue(networkRangeId), - }) - } - - return nil -} - -func toDefaultNameserversPayload(_ context.Context, model *Model) ([]string, error) { - if model == nil { - return nil, fmt.Errorf("model is nil") - } - - modelDefaultNameservers := []string{} - for _, ns := range model.Ipv4.DefaultNameservers.Elements() { - nameserverString, ok := ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString()) - } - - return modelDefaultNameservers, nil -} - -func toNetworkRangesPayload(_ context.Context, model *Model) (*[]iaas.NetworkRange, error) { - if model == nil { - return nil, fmt.Errorf("model is nil") - } - - if len(model.Ipv4.NetworkRanges) == 0 { - return nil, nil - } - - payload := []iaas.NetworkRange{} - for _, networkRange := range model.Ipv4.NetworkRanges { - payload = append(payload, iaas.NetworkRange{ - Prefix: conversion.StringValueToPointer(networkRange.Prefix), - }) - } - - return &payload, nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRegionPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } else if model.Ipv4 == nil { - return nil, fmt.Errorf("nil model.Ipv4") - } - - modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting default nameservers: %w", err) - } - - networkRangesPayload, err := toNetworkRangesPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting network ranges: %w", err) - } - - return &iaas.CreateNetworkAreaRegionPayload{ - Ipv4: &iaas.RegionalAreaIPv4{ - DefaultNameservers: &modelDefaultNameservers, - DefaultPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.DefaultPrefixLength), - MaxPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MaxPrefixLength), - MinPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MinPrefixLength), - TransferNetwork: conversion.StringValueToPointer(model.Ipv4.TransferNetwork), - NetworkRanges: networkRangesPayload, - }, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model) (*iaas.UpdateNetworkAreaRegionPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - modelDefaultNameservers, err := toDefaultNameserversPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting default nameservers: %w", err) - } - - return &iaas.UpdateNetworkAreaRegionPayload{ - Ipv4: &iaas.UpdateRegionalAreaIPv4{ - DefaultNameservers: &modelDefaultNameservers, - DefaultPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.DefaultPrefixLength), - MaxPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MaxPrefixLength), - MinPrefixLen: conversion.Int64ValueToPointer(model.Ipv4.MinPrefixLength), - }, - }, nil -} - -// updateIpv4NetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model. -func updateIpv4NetworkRanges(ctx context.Context, organizationId, networkAreaId string, ranges []networkRangeModel, client *iaas.APIClient, region string) error { - // Get network ranges current state - currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId, region).Execute() - if err != nil { - return fmt.Errorf("error reading network area ranges: %w", err) - } - - type networkRangeState struct { - isInModel bool - isCreated bool - id string - } - - networkRangesState := make(map[string]*networkRangeState) - for _, nwRange := range ranges { - networkRangesState[nwRange.Prefix.ValueString()] = &networkRangeState{ - isInModel: true, - } - } - - for _, networkRange := range *currentNetworkRangesResp.Items { - prefix := *networkRange.Prefix - if _, ok := networkRangesState[prefix]; !ok { - networkRangesState[prefix] = &networkRangeState{} - } - networkRangesState[prefix].isCreated = true - networkRangesState[prefix].id = *networkRange.Id - } - - // Delete network ranges - for prefix, state := range networkRangesState { - if !state.isInModel && state.isCreated { - err := client.DeleteNetworkAreaRange(ctx, organizationId, networkAreaId, region, state.id).Execute() - if err != nil { - return fmt.Errorf("deleting network area range '%v': %w", prefix, err) - } - } - } - - // Create network ranges - for prefix, state := range networkRangesState { - if state.isInModel && !state.isCreated { - payload := iaas.CreateNetworkAreaRangePayload{ - Ipv4: &[]iaas.NetworkRange{ - { - Prefix: sdkUtils.Ptr(prefix), - }, - }, - } - - _, err := client.CreateNetworkAreaRange(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRangePayload(payload).Execute() - if err != nil { - return fmt.Errorf("creating network range '%v': %w", prefix, err) - } - } - } - - return nil -} diff --git a/stackit/internal/services/iaas/networkarearegion/resource_test.go b/stackit/internal/services/iaas/networkarearegion/resource_test.go deleted file mode 100644 index 978ca80a..00000000 --- a/stackit/internal/services/iaas/networkarearegion/resource_test.go +++ /dev/null @@ -1,1052 +0,0 @@ -package networkarearegion - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "github.com/gorilla/mux" - "github.com/stackitcloud/stackit-sdk-go/core/config" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -const ( - testRegion = "eu01" -) - -var ( - organizationId = uuid.NewString() - networkAreaId = uuid.NewString() - - networkRangeId1 = uuid.NewString() - networkRangeId2 = uuid.NewString() - networkRangeId3 = uuid.NewString() - networkRangeId4 = uuid.NewString() - networkRangeId5 = uuid.NewString() - networkRangeId2Repeated = uuid.NewString() -) - -func Test_mapFields(t *testing.T) { - type args struct { - networkAreaRegion *iaas.RegionalArea - model *Model - region string - } - tests := []struct { - name string - args args - want *Model - wantErr bool - }{ - { - name: "default", - args: args{ - model: &Model{ - OrganizationId: types.StringValue(organizationId), - NetworkAreaId: types.StringValue(networkAreaId), - }, - networkAreaRegion: &iaas.RegionalArea{ - Ipv4: &iaas.RegionalAreaIPv4{ - DefaultNameservers: &[]string{ - "nameserver1", - "nameserver2", - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - NetworkRanges: &[]iaas.NetworkRange{ - { - Id: utils.Ptr(networkRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - { - Id: utils.Ptr(networkRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - }, - }, - }, - region: "eu01", - }, - want: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,eu01", organizationId, networkAreaId)), - OrganizationId: types.StringValue(organizationId), - NetworkAreaId: types.StringValue(networkAreaId), - Region: types.StringValue("eu01"), - - Ipv4: &ipv4Model{ - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nameserver1"), - types.StringValue("nameserver2"), - }), - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), - NetworkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("prefix-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("prefix-2"), - }, - }, - }, - }, - wantErr: false, - }, - { - name: "model is nil", - args: args{ - model: nil, - networkAreaRegion: &iaas.RegionalArea{}, - }, - want: nil, - wantErr: true, - }, - { - name: "network area region response is nil", - args: args{ - model: &Model{ - Ipv4: &ipv4Model{ - DefaultNameservers: types.ListNull(types.StringType), - NetworkRanges: []networkRangeModel{}, - }, - }, - }, - want: &Model{ - Ipv4: &ipv4Model{ - DefaultNameservers: types.ListNull(types.StringType), - NetworkRanges: []networkRangeModel{}, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - if err := mapFields(ctx, tt.args.networkAreaRegion, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { - t.Errorf("mapFields() error = %v, wantErr %v", err, tt.wantErr) - } - diff := cmp.Diff(tt.args.model, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func Test_toCreatePayload(t *testing.T) { - type args struct { - model *Model - } - tests := []struct { - name string - args args - want *iaas.CreateNetworkAreaRegionPayload - wantErr bool - }{ - { - name: "default_ok", - args: args{ - model: &Model{ - Ipv4: &ipv4Model{ - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - NetworkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringUnknown(), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringUnknown(), - Prefix: types.StringValue("pr-2"), - }, - }, - TransferNetwork: types.StringValue("network"), - DefaultPrefixLength: types.Int64Value(20), - MaxPrefixLength: types.Int64Value(22), - MinPrefixLength: types.Int64Value(18), - }, - }, - }, - want: &iaas.CreateNetworkAreaRegionPayload{ - Ipv4: &iaas.RegionalAreaIPv4{ - DefaultNameservers: &[]string{ - "ns1", - "ns2", - }, - NetworkRanges: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("pr-1"), - }, - { - Prefix: utils.Ptr("pr-2"), - }, - }, - TransferNetwork: utils.Ptr("network"), - DefaultPrefixLen: utils.Ptr(int64(20)), - MaxPrefixLen: utils.Ptr(int64(22)), - MinPrefixLen: utils.Ptr(int64(18)), - }, - }, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toCreatePayload(context.Background(), tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toCreatePayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - diff := cmp.Diff(got, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func Test_toUpdatePayload(t *testing.T) { - type args struct { - model *Model - } - tests := []struct { - name string - args args - want *iaas.UpdateNetworkAreaRegionPayload - wantErr bool - }{ - { - name: "default_ok", - args: args{ - model: &Model{ - Ipv4: &ipv4Model{ - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ns1"), - types.StringValue("ns2"), - }), - DefaultPrefixLength: types.Int64Value(22), - MaxPrefixLength: types.Int64Value(24), - MinPrefixLength: types.Int64Value(20), - }, - }, - }, - want: &iaas.UpdateNetworkAreaRegionPayload{ - Ipv4: &iaas.UpdateRegionalAreaIPv4{ - DefaultNameservers: &[]string{ - "ns1", - "ns2", - }, - DefaultPrefixLen: utils.Ptr(int64(22)), - MaxPrefixLen: utils.Ptr(int64(24)), - MinPrefixLen: utils.Ptr(int64(20)), - }, - }, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - want: nil, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toUpdatePayload(context.Background(), tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - diff := cmp.Diff(got, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func Test_mapIpv4NetworkRanges(t *testing.T) { - type args struct { - networkAreaRangesList *[]iaas.NetworkRange - model *Model - } - tests := []struct { - name string - args args - want *Model - wantErr bool - }{ - { - name: "model and response have ranges in different order", - args: args{ - model: &Model{ - OrganizationId: types.StringValue(organizationId), - NetworkAreaId: types.StringValue(networkAreaId), - Ipv4: &ipv4Model{ - DefaultNameservers: types.ListNull(types.StringType), - NetworkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("prefix-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("prefix-2"), - }, - }, - }, - }, - networkAreaRangesList: &[]iaas.NetworkRange{ - { - Id: utils.Ptr(networkRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - { - Id: utils.Ptr(networkRangeId3), - Prefix: utils.Ptr("prefix-3"), - }, - { - Id: utils.Ptr(networkRangeId1), - Prefix: utils.Ptr("prefix-1"), - }, - }, - }, - want: &Model{ - OrganizationId: types.StringValue(organizationId), - NetworkAreaId: types.StringValue(networkAreaId), - Ipv4: &ipv4Model{ - NetworkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("prefix-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("prefix-2"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("prefix-3"), - }, - }, - DefaultNameservers: types.ListNull(types.StringType), - }, - }, - wantErr: false, - }, - { - name: "network_ranges_changed_outside_tf", - args: args{ - model: &Model{ - OrganizationId: types.StringValue(organizationId), - NetworkAreaId: types.StringValue(networkAreaId), - Ipv4: &ipv4Model{ - NetworkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("prefix-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("prefix-2"), - }, - }, - DefaultNameservers: types.ListNull(types.StringType), - }, - }, - networkAreaRangesList: &[]iaas.NetworkRange{ - { - Id: utils.Ptr(networkRangeId2), - Prefix: utils.Ptr("prefix-2"), - }, - { - Id: utils.Ptr(networkRangeId3), - Prefix: utils.Ptr("prefix-3"), - }, - }, - }, - want: &Model{ - OrganizationId: types.StringValue(organizationId), - NetworkAreaId: types.StringValue(networkAreaId), - Ipv4: &ipv4Model{ - NetworkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("prefix-2"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("prefix-3"), - }, - }, - DefaultNameservers: types.ListNull(types.StringType), - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := mapIpv4NetworkRanges(context.Background(), tt.args.networkAreaRangesList, tt.args.model); (err != nil) != tt.wantErr { - t.Errorf("mapIpv4NetworkRanges() error = %v, wantErr %v", err, tt.wantErr) - } - diff := cmp.Diff(tt.args.model, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func Test_updateIpv4NetworkRanges(t *testing.T) { - getAllNetworkRangesResp := iaas.NetworkRangeListResponse{ - Items: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("pr-1"), - Id: utils.Ptr(networkRangeId1), - }, - { - Prefix: utils.Ptr("pr-2"), - Id: utils.Ptr(networkRangeId2), - }, - { - Prefix: utils.Ptr("pr-3"), - Id: utils.Ptr(networkRangeId3), - }, - { - Prefix: utils.Ptr("pr-2"), - Id: utils.Ptr(networkRangeId2Repeated), - }, - }, - } - getAllNetworkRangesRespBytes, err := json.Marshal(getAllNetworkRangesResp) - if err != nil { - t.Fatalf("Failed to marshal get all network ranges response: %v", err) - } - - // This is the response used whenever an API returns a failure response - failureRespBytes := []byte("{\"message\": \"Something bad happened\"") - - type args struct { - networkRanges []networkRangeModel - } - tests := []struct { - description string - args args - - expectedNetworkRangesStates map[string]bool // Keys are prefix; value is true if prefix should exist at the end, false if should be deleted - isValid bool - - // mock control - createNetworkRangesFails bool - deleteNetworkRangesFails bool - getAllNetworkRangesFails bool - }{ - { - description: "no_changes", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": true, - }, - isValid: true, - }, - { - description: "create_network_ranges", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId4), - Prefix: types.StringValue("pr-4"), - }, - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": true, - "pr-4": true, - }, - isValid: true, - }, - { - description: "delete_network_ranges", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": false, - "pr-3": true, - }, - isValid: true, - }, - { - description: "multiple_changes", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId4), - Prefix: types.StringValue("pr-4"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId5), - Prefix: types.StringValue("pr-5"), - }, - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": false, - "pr-3": true, - "pr-4": true, - "pr-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_repetition", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId4), - Prefix: types.StringValue("pr-4"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId5), - Prefix: types.StringValue("pr-5"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId5), - Prefix: types.StringValue("pr-5"), - }, - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": false, - "pr-3": true, - "pr-4": true, - "pr-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_2", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId4), - Prefix: types.StringValue("pr-4"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId5), - Prefix: types.StringValue("pr-5"), - }, - }, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": false, - "pr-2": false, - "pr-3": false, - "pr-4": true, - "pr-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_3", - args: args{ - networkRanges: []networkRangeModel{}, - }, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": false, - "pr-2": false, - "pr-3": false, - }, - isValid: true, - }, - { - description: "get_fails", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - }, - }, - getAllNetworkRangesFails: true, - isValid: false, - }, - { - description: "create_fails_1", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId4), - Prefix: types.StringValue("pr-4"), - }, - }, - }, - createNetworkRangesFails: true, - isValid: false, - }, - { - description: "create_fails_2", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("pr-2"), - }, - }, - }, - createNetworkRangesFails: true, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": false, - }, - isValid: true, - }, - { - description: "delete_fails_1", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("pr-2"), - }, - }, - }, - deleteNetworkRangesFails: true, - isValid: false, - }, - { - description: "delete_fails_2", - args: args{ - networkRanges: []networkRangeModel{ - { - NetworkRangeId: types.StringValue(networkRangeId1), - Prefix: types.StringValue("pr-1"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId2), - Prefix: types.StringValue("pr-2"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId3), - Prefix: types.StringValue("pr-3"), - }, - { - NetworkRangeId: types.StringValue(networkRangeId4), - Prefix: types.StringValue("pr-4"), - }, - }, - }, - deleteNetworkRangesFails: true, - expectedNetworkRangesStates: map[string]bool{ - "pr-1": true, - "pr-2": true, - "pr-3": true, - "pr-4": true, - }, - isValid: true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - // Will be compared to tt.expectedNetworkRangesStates at the end - networkRangesStates := make(map[string]bool) - networkRangesStates["pr-1"] = true - networkRangesStates["pr-2"] = true - networkRangesStates["pr-3"] = true - - // Handler for getting all network ranges - getAllNetworkRangesHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - if tt.getAllNetworkRangesFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Get all network ranges handler: failed to write bad response: %v", err) - } - return - } - - _, err := w.Write(getAllNetworkRangesRespBytes) - if err != nil { - t.Errorf("Get all network ranges handler: failed to write response: %v", err) - } - }) - - // Handler for creating network range - createNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - var payload iaas.CreateNetworkAreaRangePayload - err := decoder.Decode(&payload) - if err != nil { - t.Errorf("Create network range handler: failed to parse payload") - return - } - if payload.Ipv4 == nil { - t.Errorf("Create network range handler: nil Ipv4") - return - } - ipv4 := *payload.Ipv4 - - for _, networkRange := range ipv4 { - prefix := *networkRange.Prefix - if prefixExists, prefixWasCreated := networkRangesStates[prefix]; prefixWasCreated && prefixExists { - t.Errorf("Create network range handler: attempted to create range '%v' that already exists", *payload.Ipv4) - return - } - w.Header().Set("Content-Type", "application/json") - if tt.createNetworkRangesFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Create network ranges handler: failed to write bad response: %v", err) - } - return - } - - resp := iaas.NetworkRange{ - Prefix: utils.Ptr("prefix"), - Id: utils.Ptr("id-range"), - } - respBytes, err := json.Marshal(resp) - if err != nil { - t.Errorf("Create network range handler: failed to marshal response: %v", err) - return - } - _, err = w.Write(respBytes) - if err != nil { - t.Errorf("Create network range handler: failed to write response: %v", err) - } - networkRangesStates[prefix] = true - } - }) - - // Handler for deleting Network range - deleteNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - networkRangeId, ok := vars["networkRangeId"] - if !ok { - t.Errorf("Delete network range handler: no range ID") - return - } - - var prefix string - for _, rangeItem := range *getAllNetworkRangesResp.Items { - if *rangeItem.Id == networkRangeId { - prefix = *rangeItem.Prefix - } - } - prefixExists, prefixWasCreated := networkRangesStates[prefix] - if !prefixWasCreated { - t.Errorf("Delete network range handler: attempted to delete range '%v' that wasn't created", prefix) - return - } - if prefixWasCreated && !prefixExists { - t.Errorf("Delete network range handler: attempted to delete range '%v' that was already deleted", prefix) - return - } - - w.Header().Set("Content-Type", "application/json") - if tt.deleteNetworkRangesFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Delete network range handler: failed to write bad response: %v", err) - } - return - } - - _, err = w.Write([]byte("{}")) - if err != nil { - t.Errorf("Delete network range handler: failed to write response: %v", err) - } - networkRangesStates[prefix] = false - }) - - // Setup server and client - router := mux.NewRouter() - router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - getAllNetworkRangesHandler(w, r) - } else if r.Method == "POST" { - createNetworkRangeHandler(w, r) - } - }) - router.HandleFunc("/v2/organizations/{organizationId}/network-areas/{areaId}/regions/{region}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler) - mockedServer := httptest.NewServer(router) - defer mockedServer.Close() - client, err := iaas.NewAPIClient( - config.WithEndpoint(mockedServer.URL), - config.WithoutAuthentication(), - ) - if err != nil { - t.Fatalf("Failed to initialize client: %v", err) - } - - // Run test - err = updateIpv4NetworkRanges(context.Background(), organizationId, networkAreaId, tt.args.networkRanges, client, testRegion) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(networkRangesStates, tt.expectedNetworkRangesStates) - if diff != "" { - t.Fatalf("Network range states do not match: %s", diff) - } - } - }) - } -} - -func Test_toDefaultNameserversPayload(t *testing.T) { - type args struct { - model *Model - } - tests := []struct { - name string - args args - want []string - wantErr bool - }{ - { - name: "values_ok", - args: args{ - model: &Model{ - Ipv4: &ipv4Model{ - DefaultNameservers: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("1.1.1.1"), - types.StringValue("8.8.8.8"), - types.StringValue("9.9.9.9"), - }), - }, - }, - }, - want: []string{ - "1.1.1.1", - "8.8.8.8", - "9.9.9.9", - }, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toDefaultNameserversPayload(context.Background(), tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toDefaultNameserversPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("toDefaultNameserversPayload() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_toNetworkRangesPayload(t *testing.T) { - type args struct { - model *Model - } - tests := []struct { - name string - args args - want *[]iaas.NetworkRange - wantErr bool - }{ - { - name: "values_ok", - args: args{ - model: &Model{ - Ipv4: &ipv4Model{ - NetworkRanges: []networkRangeModel{ - { - Prefix: types.StringValue("prefix-1"), - }, - { - Prefix: types.StringValue("prefix-2"), - }, - }, - }, - }, - }, - want: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("prefix-1"), - }, - { - Prefix: utils.Ptr("prefix-2"), - }, - }, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - { - name: "network ranges is nil", - args: args{ - model: &Model{ - Ipv4: &ipv4Model{ - NetworkRanges: nil, - }, - }, - }, - want: nil, - wantErr: false, - }, - { - name: "network ranges has length 0", - args: args{ - model: &Model{ - Ipv4: &ipv4Model{ - NetworkRanges: []networkRangeModel{}, - }, - }, - }, - want: nil, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toNetworkRangesPayload(context.Background(), tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toNetworkRangesPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - diff := cmp.Diff(got, tt.want) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} diff --git a/stackit/internal/services/iaas/networkarearoute/datasource.go b/stackit/internal/services/iaas/networkarearoute/datasource.go deleted file mode 100644 index 924d90ca..00000000 --- a/stackit/internal/services/iaas/networkarearoute/datasource.go +++ /dev/null @@ -1,186 +0,0 @@ -package networkarearoute - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &networkAreaRouteDataSource{} -) - -// NewNetworkAreaRouteDataSource is a helper function to simplify the provider implementation. -func NewNetworkAreaRouteDataSource() datasource.DataSource { - return &networkAreaRouteDataSource{} -} - -// networkDataSource is the data source implementation. -type networkAreaRouteDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *networkAreaRouteDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_area_route" -} - -func (d *networkAreaRouteDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Network area route data resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`network_area_route_id`\".", - Computed: true, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the network area is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID to which the network area route is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "network_area_route_id": schema.StringAttribute{ - Description: "The network area route ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "destination": schema.SingleNestedAttribute{ - Description: "Destination of the route.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: fmt.Sprintf("CIDRV type. %s", utils.FormatPossibleValues("cidrv4", "cidrv6")), - Computed: true, - }, - "value": schema.StringAttribute{ - Description: "An CIDR string.", - Computed: true, - }, - }, - }, - "next_hop": schema.SingleNestedAttribute{ - Description: "Next hop destination.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: "Type of the next hop. " + utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), - Computed: true, - }, - "value": schema.StringAttribute{ - Description: "Either IPv4 or IPv6 (not set for blackhole and internet).", - Computed: true, - }, - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model ModelV1 - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - networkAreaRouteId := model.NetworkAreaRouteId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) - - networkAreaRouteResp, err := d.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading network area route", - fmt.Sprintf("Network area route with ID %q or network area with ID %q does not exist in organization %q.", networkAreaRouteId, networkAreaId, organizationId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, networkAreaRouteResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area route read") -} diff --git a/stackit/internal/services/iaas/networkarearoute/resource.go b/stackit/internal/services/iaas/networkarearoute/resource.go deleted file mode 100644 index f5ba3bd9..00000000 --- a/stackit/internal/services/iaas/networkarearoute/resource.go +++ /dev/null @@ -1,739 +0,0 @@ -package networkarearoute - -import ( - "context" - "fmt" - "net/http" - "strings" - - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &networkAreaRouteResource{} - _ resource.ResourceWithConfigure = &networkAreaRouteResource{} - _ resource.ResourceWithImportState = &networkAreaRouteResource{} - _ resource.ResourceWithModifyPlan = &networkAreaRouteResource{} - _ resource.ResourceWithUpgradeState = &networkAreaRouteResource{} -) - -// ModelV1 is the currently used model -type ModelV1 struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - Region types.String `tfsdk:"region"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - NetworkAreaRouteId types.String `tfsdk:"network_area_route_id"` - NextHop *NexthopModelV1 `tfsdk:"next_hop"` - Destination *DestinationModelV1 `tfsdk:"destination"` - Labels types.Map `tfsdk:"labels"` -} - -// ModelV0 is the old model (only needed for state upgrade) -type ModelV0 struct { - Id types.String `tfsdk:"id"` - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - NetworkAreaRouteId types.String `tfsdk:"network_area_route_id"` - NextHop types.String `tfsdk:"next_hop"` - Prefix types.String `tfsdk:"prefix"` - Labels types.Map `tfsdk:"labels"` -} - -// DestinationModelV1 maps the route destination data -type DestinationModelV1 struct { - Type types.String `tfsdk:"type"` - Value types.String `tfsdk:"value"` -} - -// NexthopModelV1 maps the route nexthop data -type NexthopModelV1 struct { - Type types.String `tfsdk:"type"` - Value types.String `tfsdk:"value"` -} - -// NewNetworkAreaRouteResource is a helper function to simplify the provider implementation. -func NewNetworkAreaRouteResource() resource.Resource { - return &networkAreaRouteResource{} -} - -// networkResource is the resource implementation. -type networkAreaRouteResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *networkAreaRouteResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_area_route" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *networkAreaRouteResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel ModelV1 - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel ModelV1 - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *networkAreaRouteResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the resource. -func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network area route resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: description, - Version: 1, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`region`,`network_area_route_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the network area is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID to which the network area route is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_route_id": schema.StringAttribute{ - Description: "The network area route ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "next_hop": schema.SingleNestedAttribute{ - Description: "Next hop destination.", - Required: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: fmt.Sprintf("Type of the next hop. %s %s", utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), "Only `ipv4` supported currently."), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "value": schema.StringAttribute{ - Description: "Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported currently.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.IP(false), - }, - }, - }, - }, - "destination": schema.SingleNestedAttribute{ - Description: "Destination of the route.", - Required: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: fmt.Sprintf("CIDRV type. %s %s", utils.FormatPossibleValues("cidrv4", "cidrv6"), "Only `cidrv4` is supported currently."), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "value": schema.StringAttribute{ - Description: "An CIDR string.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.CIDR(), - }, - }, - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - }, - } -} - -func (r *networkAreaRouteResource) UpgradeState(_ context.Context) map[int64]resource.StateUpgrader { - return map[int64]resource.StateUpgrader{ - 0: { - // This handles moving from version 0 to 1 - PriorSchema: &schema.Schema{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - }, - "organization_id": schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_route_id": schema.StringAttribute{ - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "next_hop": schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - validate.IP(false), - }, - }, - "prefix": schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - validate.CIDR(), - }, - }, - "labels": schema.MapAttribute{ - ElementType: types.StringType, - Optional: true, - }, - }, - }, - StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, resp *resource.UpgradeStateResponse) { - var priorStateData ModelV0 - resp.Diagnostics.Append(req.State.Get(ctx, &priorStateData)...) - if resp.Diagnostics.HasError() { - return - } - - nexthopValue := priorStateData.NextHop.ValueString() - prefixValue := priorStateData.Prefix.ValueString() - - newStateData := ModelV1{ - Id: priorStateData.Id, - OrganizationId: priorStateData.OrganizationId, - NetworkAreaId: priorStateData.NetworkAreaId, - NetworkAreaRouteId: priorStateData.NetworkAreaRouteId, - Labels: priorStateData.Labels, - - NextHop: &NexthopModelV1{ - Type: types.StringValue("ipv4"), - Value: types.StringValue(nexthopValue), - }, - Destination: &DestinationModelV1{ - Type: types.StringValue("cidrv4"), - Value: types.StringValue(prefixValue), - }, - } - - resp.Diagnostics.Append(resp.State.Set(ctx, newStateData)...) - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model ModelV1 - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkAreaId := model.NetworkAreaId.ValueString() - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network area route - routes, err := r.client.CreateNetworkAreaRoute(ctx, organizationId, networkAreaId, region).CreateNetworkAreaRoutePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if routes.Items == nil || len(*routes.Items) == 0 { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route.", "Empty response from API") - return - } - - if len(*routes.Items) != 1 { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route.", "New static route not found or more than 1 route found in API response.") - return - } - - // Gets the route ID from the first element, routes.Items[0] - routeItems := *routes.Items - route := routeItems[0] - routeId := *route.Id - - ctx = tflog.SetField(ctx, "network_area_route_id", routeId) - - // Map response body to schema - err = mapFields(ctx, &route, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route.", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area route created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model ModelV1 - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkAreaRouteId := model.NetworkAreaRouteId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) - - networkAreaRouteResp, err := r.client.GetNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route.", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, networkAreaRouteResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area route read") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model ModelV1 - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkAreaRouteId := model.NetworkAreaRouteId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) - - // Delete existing network - err := r.client.DeleteNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area route", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Network area route deleted") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model ModelV1 - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkAreaRouteId := model.NetworkAreaRouteId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_route_id", networkAreaRouteId) - - // Retrieve values from state - var stateModel ModelV1 - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing network area route - networkAreaRouteResp, err := r.client.UpdateNetworkAreaRoute(ctx, organizationId, networkAreaId, region, networkAreaRouteId).UpdateNetworkAreaRoutePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, networkAreaRouteResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network area route updated") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: organization_id,network_aread_id,network_area_route_id -func (r *networkAreaRouteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network area route", - fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[region],[network_area_route_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "organization_id": idParts[0], - "network_area_id": idParts[1], - "region": idParts[2], - "network_area_route_id": idParts[3], - }) - - tflog.Info(ctx, "Network area route state imported") -} - -func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *ModelV1, region string) error { - if networkAreaRoute == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkAreaRouteId string - if model.NetworkAreaRouteId.ValueString() != "" { - networkAreaRouteId = model.NetworkAreaRouteId.ValueString() - } else if networkAreaRoute.Id != nil { - networkAreaRouteId = *networkAreaRoute.Id - } else { - return fmt.Errorf("network area route id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), model.NetworkAreaId.ValueString(), region, networkAreaRouteId) - model.Region = types.StringValue(region) - - labels, err := iaasUtils.MapLabels(ctx, networkAreaRoute.Labels, model.Labels) - if err != nil { - return err - } - - model.NetworkAreaRouteId = types.StringValue(networkAreaRouteId) - model.Labels = labels - - model.NextHop, err = mapRouteNextHop(networkAreaRoute) - if err != nil { - return err - } - - model.Destination, err = mapRouteDestination(networkAreaRoute) - if err != nil { - return err - } - - return nil -} - -func toCreatePayload(ctx context.Context, model *ModelV1) (*iaas.CreateNetworkAreaRoutePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - nextHopPayload, err := toNextHopPayload(model) - if err != nil { - return nil, err - } - - destinationPayload, err := toDestinationPayload(model) - if err != nil { - return nil, err - } - - return &iaas.CreateNetworkAreaRoutePayload{ - Items: &[]iaas.Route{ - { - Destination: destinationPayload, - Labels: &labels, - Nexthop: nextHopPayload, - }, - }, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *ModelV1, currentLabels types.Map) (*iaas.UpdateNetworkAreaRoutePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.UpdateNetworkAreaRoutePayload{ - Labels: &labels, - }, nil -} - -func toNextHopPayload(model *ModelV1) (*iaas.RouteNexthop, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } else if model.NextHop == nil { - return nil, fmt.Errorf("nexthop is nil in model") - } - - switch model.NextHop.Type.ValueString() { - case "blackhole": - return sdkUtils.Ptr(iaas.NexthopBlackholeAsRouteNexthop(iaas.NewNexthopBlackhole("blackhole"))), nil - case "internet": - return sdkUtils.Ptr(iaas.NexthopInternetAsRouteNexthop(iaas.NewNexthopInternet("internet"))), nil - case "ipv4": - return sdkUtils.Ptr(iaas.NexthopIPv4AsRouteNexthop(iaas.NewNexthopIPv4("ipv4", model.NextHop.Value.ValueString()))), nil - case "ipv6": - return sdkUtils.Ptr(iaas.NexthopIPv6AsRouteNexthop(iaas.NewNexthopIPv6("ipv6", model.NextHop.Value.ValueString()))), nil - } - return nil, fmt.Errorf("unknown nexthop type: %s", model.NextHop.Type.ValueString()) -} - -func toDestinationPayload(model *ModelV1) (*iaas.RouteDestination, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } else if model.Destination == nil { - return nil, fmt.Errorf("destination is nil in model") - } - - switch model.Destination.Type.ValueString() { - case "cidrv4": - return sdkUtils.Ptr(iaas.DestinationCIDRv4AsRouteDestination(iaas.NewDestinationCIDRv4("cidrv4", model.Destination.Value.ValueString()))), nil - case "cidrv6": - return sdkUtils.Ptr(iaas.DestinationCIDRv6AsRouteDestination(iaas.NewDestinationCIDRv6("cidrv6", model.Destination.Value.ValueString()))), nil - } - return nil, fmt.Errorf("unknown destination type: %s", model.Destination.Type.ValueString()) -} - -func mapRouteNextHop(routeResp *iaas.Route) (*NexthopModelV1, error) { - if routeResp.Nexthop == nil { - return &NexthopModelV1{ - Type: types.StringNull(), - Value: types.StringNull(), - }, nil - } - - switch i := routeResp.Nexthop.GetActualInstance().(type) { - case *iaas.NexthopIPv4: - return &NexthopModelV1{ - Type: types.StringPointerValue(i.Type), - Value: types.StringPointerValue(i.Value), - }, nil - case *iaas.NexthopIPv6: - return &NexthopModelV1{ - Type: types.StringPointerValue(i.Type), - Value: types.StringPointerValue(i.Value), - }, nil - case *iaas.NexthopBlackhole: - return &NexthopModelV1{ - Type: types.StringPointerValue(i.Type), - Value: types.StringNull(), - }, nil - case *iaas.NexthopInternet: - return &NexthopModelV1{ - Type: types.StringPointerValue(i.Type), - Value: types.StringNull(), - }, nil - default: - return nil, fmt.Errorf("unexpected nexthop type: %T", i) - } -} - -func mapRouteDestination(routeResp *iaas.Route) (*DestinationModelV1, error) { - if routeResp.Destination == nil { - return &DestinationModelV1{ - Type: types.StringNull(), - Value: types.StringNull(), - }, nil - } - - switch i := routeResp.Destination.GetActualInstance().(type) { - case *iaas.DestinationCIDRv4: - return &DestinationModelV1{ - Type: types.StringPointerValue(i.Type), - Value: types.StringPointerValue(i.Value), - }, nil - case *iaas.DestinationCIDRv6: - return &DestinationModelV1{ - Type: types.StringPointerValue(i.Type), - Value: types.StringPointerValue(i.Value), - }, nil - default: - return nil, fmt.Errorf("unexpected Destionation type: %T", i) - } -} diff --git a/stackit/internal/services/iaas/networkarearoute/resource_test.go b/stackit/internal/services/iaas/networkarearoute/resource_test.go deleted file mode 100644 index a0295cf3..00000000 --- a/stackit/internal/services/iaas/networkarearoute/resource_test.go +++ /dev/null @@ -1,623 +0,0 @@ -package networkarearoute - -import ( - "context" - "reflect" - "testing" - - "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state ModelV1 - input *iaas.Route - region string - } - tests := []struct { - description string - args args - expected ModelV1 - isValid bool - }{ - { - description: "id_ok", - args: args{ - state: ModelV1{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkAreaRouteId: types.StringValue("narid"), - }, - input: &iaas.Route{}, - region: "eu01", - }, - expected: ModelV1{ - Id: types.StringValue("oid,naid,eu01,narid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkAreaRouteId: types.StringValue("narid"), - Destination: &DestinationModelV1{ - Type: types.StringNull(), - Value: types.StringNull(), - }, - NextHop: &NexthopModelV1{ - Type: types.StringNull(), - Value: types.StringNull(), - }, - Labels: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "values_ok", - args: args{ - state: ModelV1{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkAreaRouteId: types.StringValue("narid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.Route{ - Destination: &iaas.RouteDestination{ - DestinationCIDRv4: &iaas.DestinationCIDRv4{ - Type: utils.Ptr("cidrv4"), - Value: utils.Ptr("prefix"), - }, - DestinationCIDRv6: nil, - }, - Nexthop: &iaas.RouteNexthop{ - NexthopIPv4: &iaas.NexthopIPv4{ - Type: utils.Ptr("ipv4"), - Value: utils.Ptr("hop"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - region: "eu02", - }, - expected: ModelV1{ - Id: types.StringValue("oid,naid,eu02,narid"), - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - NetworkAreaRouteId: types.StringValue("narid"), - Destination: &DestinationModelV1{ - Type: types.StringValue("cidrv4"), - Value: types.StringValue("prefix"), - }, - NextHop: &NexthopModelV1{ - Type: types.StringValue("ipv4"), - Value: types.StringValue("hop"), - }, - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "response_fields_nil_fail", - args: args{ - input: &iaas.Route{ - Destination: nil, - Nexthop: nil, - }, - }, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: ModelV1{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - }, - input: &iaas.Route{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *ModelV1 - expected *iaas.CreateNetworkAreaRoutePayload - isValid bool - }{ - { - description: "default_ok", - input: &ModelV1{ - Destination: &DestinationModelV1{ - Type: types.StringValue("cidrv4"), - Value: types.StringValue("prefix"), - }, - NextHop: &NexthopModelV1{ - Type: types.StringValue("ipv4"), - Value: types.StringValue("hop"), - }, - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - expected: &iaas.CreateNetworkAreaRoutePayload{ - Items: &[]iaas.Route{ - { - Destination: &iaas.RouteDestination{ - DestinationCIDRv4: &iaas.DestinationCIDRv4{ - Type: utils.Ptr("cidrv4"), - Value: utils.Ptr("prefix"), - }, - DestinationCIDRv6: nil, - }, - Nexthop: &iaas.RouteNexthop{ - NexthopIPv4: &iaas.NexthopIPv4{ - Type: utils.Ptr("ipv4"), - Value: utils.Ptr("hop"), - }, - }, - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - }, - }, - isValid: true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *ModelV1 - expected *iaas.UpdateNetworkAreaRoutePayload - isValid bool - }{ - { - "default_ok", - &ModelV1{ - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key1": types.StringValue("value1"), - "key2": types.StringValue("value2"), - }), - }, - &iaas.UpdateNetworkAreaRoutePayload{ - Labels: &map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToNextHopPayload(t *testing.T) { - type args struct { - model *ModelV1 - } - tests := []struct { - name string - args args - want *iaas.RouteNexthop - wantErr bool - }{ - { - name: "ipv4", - args: args{ - model: &ModelV1{ - NextHop: &NexthopModelV1{ - Type: types.StringValue("ipv4"), - Value: types.StringValue("10.20.30.40"), - }, - }, - }, - want: &iaas.RouteNexthop{ - NexthopIPv4: &iaas.NexthopIPv4{ - Type: utils.Ptr("ipv4"), - Value: utils.Ptr("10.20.30.40"), - }, - }, - wantErr: false, - }, - { - name: "ipv6", - args: args{ - model: &ModelV1{ - NextHop: &NexthopModelV1{ - Type: types.StringValue("ipv6"), - Value: types.StringValue("2001:db8:85a3:0:0:8a2e:370:7334"), - }, - }, - }, - want: &iaas.RouteNexthop{ - NexthopIPv6: &iaas.NexthopIPv6{ - Type: utils.Ptr("ipv6"), - Value: utils.Ptr("2001:db8:85a3:0:0:8a2e:370:7334"), - }, - }, - wantErr: false, - }, - { - name: "internet", - args: args{ - model: &ModelV1{ - NextHop: &NexthopModelV1{ - Type: types.StringValue("internet"), - }, - }, - }, - want: &iaas.RouteNexthop{ - NexthopInternet: &iaas.NexthopInternet{ - Type: utils.Ptr("internet"), - }, - }, - wantErr: false, - }, - { - name: "blackhole", - args: args{ - model: &ModelV1{ - NextHop: &NexthopModelV1{ - Type: types.StringValue("blackhole"), - }, - }, - }, - want: &iaas.RouteNexthop{ - NexthopBlackhole: &iaas.NexthopBlackhole{ - Type: utils.Ptr("blackhole"), - }, - }, - wantErr: false, - }, - { - name: "invalid type", - args: args{ - model: &ModelV1{ - NextHop: &NexthopModelV1{ - Type: types.StringValue("foobar"), - }, - }, - }, - wantErr: true, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - { - name: "nexthop in model is nil", - args: args{ - model: &ModelV1{ - NextHop: nil, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toNextHopPayload(tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toNextHopPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("toNextHopPayload() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestToDestinationPayload(t *testing.T) { - type args struct { - model *ModelV1 - } - tests := []struct { - name string - args args - want *iaas.RouteDestination - wantErr bool - }{ - { - name: "cidrv4", - args: args{ - model: &ModelV1{ - Destination: &DestinationModelV1{ - Type: types.StringValue("cidrv4"), - Value: types.StringValue("192.168.1.0/24"), - }, - }, - }, - want: &iaas.RouteDestination{ - DestinationCIDRv4: &iaas.DestinationCIDRv4{ - Type: utils.Ptr("cidrv4"), - Value: utils.Ptr("192.168.1.0/24"), - }, - }, - wantErr: false, - }, - { - name: "cidrv6", - args: args{ - model: &ModelV1{ - Destination: &DestinationModelV1{ - Type: types.StringValue("cidrv6"), - Value: types.StringValue("2001:db8:1234::/48"), - }, - }, - }, - want: &iaas.RouteDestination{ - DestinationCIDRv6: &iaas.DestinationCIDRv6{ - Type: utils.Ptr("cidrv6"), - Value: utils.Ptr("2001:db8:1234::/48"), - }, - }, - wantErr: false, - }, - { - name: "invalid type", - args: args{ - model: &ModelV1{ - Destination: &DestinationModelV1{ - Type: types.StringValue("foobar"), - }, - }, - }, - wantErr: true, - }, - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - { - name: "destination in model is nil", - args: args{ - model: &ModelV1{ - Destination: nil, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toDestinationPayload(tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toDestinationPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("toDestinationPayload() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMapRouteNextHop(t *testing.T) { - type args struct { - routeResp *iaas.Route - } - tests := []struct { - name string - args args - want *NexthopModelV1 - wantErr bool - }{ - { - name: "ipv4", - args: args{ - routeResp: &iaas.Route{ - Nexthop: &iaas.RouteNexthop{ - NexthopIPv4: &iaas.NexthopIPv4{ - Type: utils.Ptr("ipv4"), - Value: utils.Ptr("192.168.1.0/24"), - }, - }, - }, - }, - want: &NexthopModelV1{ - Type: types.StringValue("ipv4"), - Value: types.StringValue("192.168.1.0/24"), - }, - }, - { - name: "ipv6", - args: args{ - routeResp: &iaas.Route{ - Nexthop: &iaas.RouteNexthop{ - NexthopIPv4: &iaas.NexthopIPv4{ - Type: utils.Ptr("ipv6"), - Value: utils.Ptr("2001:db8:85a3:0:0:8a2e:370:7334"), - }, - }, - }, - }, - want: &NexthopModelV1{ - Type: types.StringValue("ipv6"), - Value: types.StringValue("2001:db8:85a3:0:0:8a2e:370:7334"), - }, - }, - { - name: "blackhole", - args: args{ - routeResp: &iaas.Route{ - Nexthop: &iaas.RouteNexthop{ - NexthopBlackhole: &iaas.NexthopBlackhole{ - Type: utils.Ptr("blackhole"), - }, - }, - }, - }, - want: &NexthopModelV1{ - Type: types.StringValue("blackhole"), - }, - }, - { - name: "internet", - args: args{ - routeResp: &iaas.Route{ - Nexthop: &iaas.RouteNexthop{ - NexthopInternet: &iaas.NexthopInternet{ - Type: utils.Ptr("internet"), - }, - }, - }, - }, - want: &NexthopModelV1{ - Type: types.StringValue("internet"), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := mapRouteNextHop(tt.args.routeResp) - if (err != nil) != tt.wantErr { - t.Errorf("mapRouteNextHop() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("mapRouteNextHop() got = %v, want %v", got, tt.want) - } - }) - } -} - -func TestMapRouteDestination(t *testing.T) { - type args struct { - routeResp *iaas.Route - } - tests := []struct { - name string - args args - want *DestinationModelV1 - wantErr bool - }{ - { - name: "cidrv4", - args: args{ - routeResp: &iaas.Route{ - Destination: &iaas.RouteDestination{ - DestinationCIDRv4: &iaas.DestinationCIDRv4{ - Type: utils.Ptr("cidrv4"), - Value: utils.Ptr("192.168.1.0/24"), - }, - }, - }, - }, - want: &DestinationModelV1{ - Type: types.StringValue("cidrv4"), - Value: types.StringValue("192.168.1.0/24"), - }, - }, - { - name: "cidrv6", - args: args{ - routeResp: &iaas.Route{ - Destination: &iaas.RouteDestination{ - DestinationCIDRv4: &iaas.DestinationCIDRv4{ - Type: utils.Ptr("cidrv6"), - Value: utils.Ptr("2001:db8:1234::/48"), - }, - }, - }, - }, - want: &DestinationModelV1{ - Type: types.StringValue("cidrv6"), - Value: types.StringValue("2001:db8:1234::/48"), - }, - }, - { - name: "destination in API response is nil", - args: args{ - routeResp: &iaas.Route{ - Destination: nil, - }, - }, - want: &DestinationModelV1{ - Type: types.StringNull(), - Value: types.StringNull(), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := mapRouteDestination(tt.args.routeResp) - if (err != nil) != tt.wantErr { - t.Errorf("mapRouteDestination() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("mapRouteDestination() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/stackit/internal/services/iaas/networkinterface/datasource.go b/stackit/internal/services/iaas/networkinterface/datasource.go deleted file mode 100644 index ad51f76a..00000000 --- a/stackit/internal/services/iaas/networkinterface/datasource.go +++ /dev/null @@ -1,193 +0,0 @@ -package networkinterface - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &networkInterfaceDataSource{} -) - -// NewNetworkInterfaceDataSource is a helper function to simplify the provider implementation. -func NewNetworkInterfaceDataSource() datasource.DataSource { - return &networkInterfaceDataSource{} -} - -// networkInterfaceDataSource is the data source implementation. -type networkInterfaceDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *networkInterfaceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_interface" -} - -func (d *networkInterfaceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - typeOptions := []string{"server", "metadata", "gateway"} - description := "Network interface datasource schema. Must have a `region` specified in the provider configuration." - - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the network interface is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "network_id": schema.StringAttribute{ - Description: "The network ID to which the network interface is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_interface_id": schema.StringAttribute{ - Description: "The network interface ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the network interface.", - Computed: true, - }, - "allowed_addresses": schema.ListAttribute{ - Description: "The list of CIDR (Classless Inter-Domain Routing) notations.", - Computed: true, - ElementType: types.StringType, - }, - "device": schema.StringAttribute{ - Description: "The device UUID of the network interface.", - Computed: true, - }, - "ipv4": schema.StringAttribute{ - Description: "The IPv4 address.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a network interface.", - ElementType: types.StringType, - Computed: true, - }, - "mac": schema.StringAttribute{ - Description: "The MAC address of network interface.", - Computed: true, - }, - "security": schema.BoolAttribute{ - Description: "The Network Interface Security. If set to false, then no security groups will apply to this network interface.", - Computed: true, - }, - "security_group_ids": schema.ListAttribute{ - Description: "The list of security group UUIDs. If security is set to false, setting this field will lead to an error.", - Computed: true, - ElementType: types.StringType, - }, - "type": schema.StringAttribute{ - Description: "Type of network interface. Some of the possible values are: " + utils.FormatPossibleValues(typeOptions...), - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - networkId := model.NetworkId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - networkInterfaceResp, err := d.client.GetNic(ctx, projectId, region, networkId, networkInterfaceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading network interface", - fmt.Sprintf("Network interface with ID %q or network with ID %q does not exist in project %q.", networkInterfaceId, networkId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, networkInterfaceResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network interface read") -} diff --git a/stackit/internal/services/iaas/networkinterface/resource.go b/stackit/internal/services/iaas/networkinterface/resource.go deleted file mode 100644 index 8ced0477..00000000 --- a/stackit/internal/services/iaas/networkinterface/resource.go +++ /dev/null @@ -1,683 +0,0 @@ -package networkinterface - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &networkInterfaceResource{} - _ resource.ResourceWithConfigure = &networkInterfaceResource{} - _ resource.ResourceWithImportState = &networkInterfaceResource{} - _ resource.ResourceWithModifyPlan = &networkInterfaceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - NetworkId types.String `tfsdk:"network_id"` - Region types.String `tfsdk:"region"` - NetworkInterfaceId types.String `tfsdk:"network_interface_id"` - Name types.String `tfsdk:"name"` - AllowedAddresses types.List `tfsdk:"allowed_addresses"` - IPv4 types.String `tfsdk:"ipv4"` - Labels types.Map `tfsdk:"labels"` - Security types.Bool `tfsdk:"security"` - SecurityGroupIds types.List `tfsdk:"security_group_ids"` - Device types.String `tfsdk:"device"` - Mac types.String `tfsdk:"mac"` - Type types.String `tfsdk:"type"` -} - -// NewNetworkInterfaceResource is a helper function to simplify the provider implementation. -func NewNetworkInterfaceResource() resource.Resource { - return &networkInterfaceResource{} -} - -// networkResource is the resource implementation. -type networkInterfaceResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -func (r *networkInterfaceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - var configModel Model - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - // If allowed_addresses were completly removed from the config this is not recognized by terraform - // since this field is optional and computed therefore this plan modifier is needed. - utils.CheckListRemoval(ctx, configModel.AllowedAddresses, planModel.AllowedAddresses, path.Root("allowed_addresses"), types.StringType, false, resp) - if resp.Diagnostics.HasError() { - return - } - - // If security_group_ids were completly removed from the config this is not recognized by terraform - // since this field is optional and computed therefore this plan modifier is needed. - utils.CheckListRemoval(ctx, configModel.SecurityGroupIds, planModel.SecurityGroupIds, path.Root("security_group_ids"), types.StringType, true, resp) - if resp.Diagnostics.HasError() { - return - } - - // Use the modifier to set the effective region in the current plan. - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *networkInterfaceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_network_interface" -} - -// Configure adds the provider configured client to the resource. -func (r *networkInterfaceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - typeOptions := []string{"server", "metadata", "gateway"} - description := "Network interface resource schema. Must have a `region` specified in the provider configuration." - - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`network_id`,`network_interface_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the network is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_id": schema.StringAttribute{ - Description: "The network ID to which the network interface is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_interface_id": schema.StringAttribute{ - Description: "The network interface ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the network interface.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "allowed_addresses": schema.ListAttribute{ - Description: "The list of CIDR (Classless Inter-Domain Routing) notations.", - Optional: true, - Computed: true, - ElementType: types.StringType, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - validate.CIDR(), - ), - }, - }, - "device": schema.StringAttribute{ - Description: "The device UUID of the network interface.", - Computed: true, - }, - "ipv4": schema.StringAttribute{ - Description: "The IPv4 address.", - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.IP(false), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a network interface.", - ElementType: types.StringType, - Optional: true, - }, - "mac": schema.StringAttribute{ - Description: "The MAC address of network interface.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "security": schema.BoolAttribute{ - Description: "The Network Interface Security. If set to false, then no security groups will apply to this network interface.", - Computed: true, - Optional: true, - }, - "security_group_ids": schema.ListAttribute{ - Description: "The list of security group UUIDs. If security is set to false, setting this field will lead to an error.", - Optional: true, - Computed: true, - ElementType: types.StringType, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`), - "must match expression"), - ), - }, - }, - "type": schema.StringAttribute{ - Description: "Type of network interface. Some of the possible values are: " + utils.FormatPossibleValues(typeOptions...), - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *networkInterfaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkId := model.NetworkId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_id", networkId) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new network interface - networkInterface, err := r.client.CreateNic(ctx, projectId, region, networkId).CreateNicPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - networkInterfaceId := *networkInterface.Id - - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - // Map response body to schema - err = mapFields(ctx, networkInterface, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network interface created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkId := model.NetworkId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - networkInterfaceResp, err := r.client.GetNic(ctx, projectId, region, networkId, networkInterfaceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, networkInterfaceResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network interface read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *networkInterfaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkId := model.NetworkId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing network - nicResp, err := r.client.UpdateNic(ctx, projectId, region, networkId, networkInterfaceId).UpdateNicPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, nicResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network interface updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - networkId := model.NetworkId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_id", networkId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - // Delete existing network interface - err := r.client.DeleteNic(ctx, projectId, region, networkId, networkInterfaceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network interface", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Network interface deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id,network_interface_id -func (r *networkInterfaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network interface", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id],[network_interface_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "network_id": idParts[2], - "network_interface_id": idParts[3], - }) - - tflog.Info(ctx, "Network interface state imported") -} - -func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model, region string) error { - if networkInterfaceResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var networkInterfaceId string - if model.NetworkInterfaceId.ValueString() != "" { - networkInterfaceId = model.NetworkInterfaceId.ValueString() - } else if networkInterfaceResp.NetworkId != nil { - networkInterfaceId = *networkInterfaceResp.Id - } else { - return fmt.Errorf("network interface id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.NetworkId.ValueString(), networkInterfaceId) - model.Region = types.StringValue(region) - - respAllowedAddresses := []string{} - var diags diag.Diagnostics - if networkInterfaceResp.AllowedAddresses == nil { - // If we send an empty list, the API will send null in the response - // We should handle this case and set the value to an empty list - if !model.AllowedAddresses.IsNull() { - model.AllowedAddresses, diags = types.ListValueFrom(ctx, types.StringType, []string{}) - if diags.HasError() { - return fmt.Errorf("map network interface allowed addresses: %w", core.DiagsToError(diags)) - } - } else { - model.AllowedAddresses = types.ListNull(types.StringType) - } - } else { - for _, n := range *networkInterfaceResp.AllowedAddresses { - respAllowedAddresses = append(respAllowedAddresses, *n.String) - } - - modelAllowedAddresses, err := utils.ListValuetoStringSlice(model.AllowedAddresses) - if err != nil { - return fmt.Errorf("get current network interface allowed addresses from model: %w", err) - } - - reconciledAllowedAddresses := utils.ReconcileStringSlices(modelAllowedAddresses, respAllowedAddresses) - - allowedAddressesTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledAllowedAddresses) - if diags.HasError() { - return fmt.Errorf("map network interface allowed addresses: %w", core.DiagsToError(diags)) - } - - model.AllowedAddresses = allowedAddressesTF - } - - if networkInterfaceResp.SecurityGroups == nil { - model.SecurityGroupIds = types.ListNull(types.StringType) - } else { - respSecurityGroups := *networkInterfaceResp.SecurityGroups - modelSecurityGroups, err := utils.ListValuetoStringSlice(model.SecurityGroupIds) - if err != nil { - return fmt.Errorf("get current network interface security groups from model: %w", err) - } - - reconciledSecurityGroups := utils.ReconcileStringSlices(modelSecurityGroups, respSecurityGroups) - - securityGroupsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledSecurityGroups) - if diags.HasError() { - return fmt.Errorf("map network interface security groups: %w", core.DiagsToError(diags)) - } - - model.SecurityGroupIds = securityGroupsTF - } - - labels, err := iaasUtils.MapLabels(ctx, networkInterfaceResp.Labels, model.Labels) - if err != nil { - return err - } - - networkInterfaceName := types.StringNull() - if networkInterfaceResp.Name != nil && *networkInterfaceResp.Name != "" { - networkInterfaceName = types.StringPointerValue(networkInterfaceResp.Name) - } - - model.NetworkInterfaceId = types.StringValue(networkInterfaceId) - model.Name = networkInterfaceName - model.IPv4 = types.StringPointerValue(networkInterfaceResp.Ipv4) - model.Security = types.BoolPointerValue(networkInterfaceResp.NicSecurity) - model.Device = types.StringPointerValue(networkInterfaceResp.Device) - model.Mac = types.StringPointerValue(networkInterfaceResp.Mac) - model.Type = types.StringPointerValue(networkInterfaceResp.Type) - model.Labels = labels - - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNicPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var labelPayload *map[string]interface{} - - modelSecurityGroups := []string{} - if !(model.SecurityGroupIds.IsNull() || model.SecurityGroupIds.IsUnknown()) { - for _, ns := range model.SecurityGroupIds.Elements() { - securityGroupString, ok := ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelSecurityGroups = append(modelSecurityGroups, securityGroupString.ValueString()) - } - } - - allowedAddressesPayload := &[]iaas.AllowedAddressesInner{} - if !(model.AllowedAddresses.IsNull() || model.AllowedAddresses.IsUnknown()) { - for _, allowedAddressModel := range model.AllowedAddresses.Elements() { - allowedAddressString, ok := allowedAddressModel.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - - *allowedAddressesPayload = append(*allowedAddressesPayload, iaas.AllowedAddressesInner{ - String: conversion.StringValueToPointer(allowedAddressString), - }) - } - } else { - allowedAddressesPayload = nil - } - - if !model.Labels.IsNull() && !model.Labels.IsUnknown() { - labelMap, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("mapping labels: %w", err) - } - labelPayload = &labelMap - } - - return &iaas.CreateNicPayload{ - AllowedAddresses: allowedAddressesPayload, - SecurityGroups: &modelSecurityGroups, - Labels: labelPayload, - Name: conversion.StringValueToPointer(model.Name), - Device: conversion.StringValueToPointer(model.Device), - Ipv4: conversion.StringValueToPointer(model.IPv4), - Mac: conversion.StringValueToPointer(model.Mac), - Type: conversion.StringValueToPointer(model.Type), - NicSecurity: conversion.BoolValueToPointer(model.Security), - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateNicPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var labelPayload *map[string]interface{} - - modelSecurityGroups := []string{} - for _, ns := range model.SecurityGroupIds.Elements() { - securityGroupString, ok := ns.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - modelSecurityGroups = append(modelSecurityGroups, securityGroupString.ValueString()) - } - - allowedAddressesPayload := []iaas.AllowedAddressesInner{} // Even if null in the model, we need to send an empty list to the API since it's a PATCH endpoint - if !(model.AllowedAddresses.IsNull() || model.AllowedAddresses.IsUnknown()) { - for _, allowedAddressModel := range model.AllowedAddresses.Elements() { - allowedAddressString, ok := allowedAddressModel.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - - allowedAddressesPayload = append(allowedAddressesPayload, iaas.AllowedAddressesInner{ - String: conversion.StringValueToPointer(allowedAddressString), - }) - } - } - - if !model.Labels.IsNull() && !model.Labels.IsUnknown() { - labelMap, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("mapping labels: %w", err) - } - labelPayload = &labelMap - } - - return &iaas.UpdateNicPayload{ - AllowedAddresses: &allowedAddressesPayload, - SecurityGroups: &modelSecurityGroups, - Labels: labelPayload, - Name: conversion.StringValueToPointer(model.Name), - NicSecurity: conversion.BoolValueToPointer(model.Security), - }, nil -} diff --git a/stackit/internal/services/iaas/networkinterface/resource_test.go b/stackit/internal/services/iaas/networkinterface/resource_test.go deleted file mode 100644 index e549f7d3..00000000 --- a/stackit/internal/services/iaas/networkinterface/resource_test.go +++ /dev/null @@ -1,368 +0,0 @@ -package networkinterface - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.NIC - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "id_ok", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - }, - input: &iaas.NIC{ - Id: utils.Ptr("nicid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,nid,nicid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - Name: types.StringNull(), - AllowedAddresses: types.ListNull(types.StringType), - SecurityGroupIds: types.ListNull(types.StringType), - IPv4: types.StringNull(), - Security: types.BoolNull(), - Device: types.StringNull(), - Mac: types.StringNull(), - Type: types.StringNull(), - Labels: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "values_ok", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.NIC{ - Id: utils.Ptr("nicid"), - Name: utils.Ptr("name"), - AllowedAddresses: &[]iaas.AllowedAddressesInner{ - { - String: utils.Ptr("aa1"), - }, - }, - SecurityGroups: &[]string{ - "prefix1", - "prefix2", - }, - Ipv4: utils.Ptr("ipv4"), - Ipv6: utils.Ptr("ipv6"), - NicSecurity: utils.Ptr(true), - Device: utils.Ptr("device"), - Mac: utils.Ptr("mac"), - Status: utils.Ptr("status"), - Type: utils.Ptr("type"), - Labels: &map[string]interface{}{ - "label1": "ref1", - }, - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,nid,nicid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - Name: types.StringValue("name"), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("aa1"), - }), - SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("prefix1"), - types.StringValue("prefix2"), - }), - IPv4: types.StringValue("ipv4"), - Security: types.BoolValue(true), - Device: types.StringValue("device"), - Mac: types.StringValue("mac"), - Type: types.StringValue("type"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"label1": types.StringValue("ref1")}), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "allowed_addresses_changed_outside_tf", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("aa1"), - }), - }, - input: &iaas.NIC{ - Id: utils.Ptr("nicid"), - AllowedAddresses: &[]iaas.AllowedAddressesInner{ - { - String: utils.Ptr("aa2"), - }, - }, - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,nid,nicid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - Name: types.StringNull(), - SecurityGroupIds: types.ListNull(types.StringType), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("aa2"), - }), - Labels: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "empty_list_allowed_addresses", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}), - }, - input: &iaas.NIC{ - Id: utils.Ptr("nicid"), - AllowedAddresses: nil, - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,nid,nicid"), - ProjectId: types.StringValue("pid"), - NetworkId: types.StringValue("nid"), - NetworkInterfaceId: types.StringValue("nicid"), - Name: types.StringNull(), - SecurityGroupIds: types.ListNull(types.StringType), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{}), - Labels: types.MapNull(types.StringType), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - args: args{ - state: Model{}, - input: nil, - }, - expected: Model{}, - isValid: false, - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.NIC{}, - }, - expected: Model{}, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateNicPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("sg1"), - types.StringValue("sg2"), - }), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("aa1"), - }), - Security: types.BoolValue(true), - }, - &iaas.CreateNicPayload{ - Name: utils.Ptr("name"), - SecurityGroups: &[]string{ - "sg1", - "sg2", - }, - AllowedAddresses: &[]iaas.AllowedAddressesInner{ - { - String: utils.Ptr("aa1"), - }, - }, - NicSecurity: utils.Ptr(true), - }, - true, - }, - { - "empty_allowed_addresses", - &Model{ - Name: types.StringValue("name"), - SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("sg1"), - types.StringValue("sg2"), - }), - - AllowedAddresses: types.ListNull(types.StringType), - }, - &iaas.CreateNicPayload{ - Name: utils.Ptr("name"), - SecurityGroups: &[]string{ - "sg1", - "sg2", - }, - AllowedAddresses: nil, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdateNicPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("sg1"), - types.StringValue("sg2"), - }), - AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("aa1"), - }), - Security: types.BoolValue(true), - }, - &iaas.UpdateNicPayload{ - Name: utils.Ptr("name"), - SecurityGroups: &[]string{ - "sg1", - "sg2", - }, - AllowedAddresses: &[]iaas.AllowedAddressesInner{ - { - String: utils.Ptr("aa1"), - }, - }, - NicSecurity: utils.Ptr(true), - }, - true, - }, - { - "empty_allowed_addresses", - &Model{ - Name: types.StringValue("name"), - SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("sg1"), - types.StringValue("sg2"), - }), - - AllowedAddresses: types.ListNull(types.StringType), - }, - &iaas.UpdateNicPayload{ - Name: utils.Ptr("name"), - SecurityGroups: &[]string{ - "sg1", - "sg2", - }, - AllowedAddresses: utils.Ptr([]iaas.AllowedAddressesInner{}), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/networkinterfaceattach/resource.go b/stackit/internal/services/iaas/networkinterfaceattach/resource.go deleted file mode 100644 index 2b8d4240..00000000 --- a/stackit/internal/services/iaas/networkinterfaceattach/resource.go +++ /dev/null @@ -1,328 +0,0 @@ -package networkinterfaceattach - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &networkInterfaceAttachResource{} - _ resource.ResourceWithConfigure = &networkInterfaceAttachResource{} - _ resource.ResourceWithImportState = &networkInterfaceAttachResource{} - _ resource.ResourceWithModifyPlan = &networkInterfaceAttachResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ServerId types.String `tfsdk:"server_id"` - NetworkInterfaceId types.String `tfsdk:"network_interface_id"` -} - -// NewNetworkInterfaceAttachResource is a helper function to simplify the provider implementation. -func NewNetworkInterfaceAttachResource() resource.Resource { - return &networkInterfaceAttachResource{} -} - -// networkInterfaceAttachResource is the resource implementation. -type networkInterfaceAttachResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_network_interface_attach" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *networkInterfaceAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Network interface attachment resource schema. Attaches a network interface to a server. The attachment only takes full effect after server reboot." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`network_interface_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the network interface attachment is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "The server ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_interface_id": schema.StringAttribute{ - Description: "The network interface ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - // Create new network interface attachment - err := r.client.AddNicToServer(ctx, projectId, region, serverId, networkInterfaceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching network interface to server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, networkInterfaceId) - model.Region = types.StringValue(region) - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Network interface attachment created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - nics, err := r.client.ListServerNICs(ctx, projectId, region, serverId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface attachment", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if nics == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface attachment", "List of network interfaces attached to the server is nil") - return - } - - if nics.Items != nil { - for _, nic := range *nics.Items { - if nic.Id == nil || (nic.Id != nil && *nic.Id != networkInterfaceId) { - continue - } - - model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, networkInterfaceId) - model.Region = types.StringValue(region) - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Network interface attachment read") - return - } - } - - // no matching network interface was found, the attachment no longer exists - resp.State.RemoveResource(ctx) -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update is not supported, all fields require replace -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - network_interfaceId := model.NetworkInterfaceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId) - - // Remove network_interface from server - err := r.client.RemoveNicFromServer(ctx, projectId, region, serverId, network_interfaceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing network interface from server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Network interface attachment deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id -func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing network_interface attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[network_interface_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ - "project_id": idParts[0], - "region": idParts[1], - "server_id": idParts[2], - "network_interface_id": idParts[3], - }) - - tflog.Info(ctx, "Network interface attachment state imported") -} diff --git a/stackit/internal/services/iaas/project/datasource.go b/stackit/internal/services/iaas/project/datasource.go deleted file mode 100644 index ac2c0ec4..00000000 --- a/stackit/internal/services/iaas/project/datasource.go +++ /dev/null @@ -1,219 +0,0 @@ -package project - -import ( - "context" - "fmt" - "time" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -var ( - _ datasource.DataSourceWithConfigure = &projectDataSource{} -) - -type DatasourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - AreaId types.String `tfsdk:"area_id"` - InternetAccess types.Bool `tfsdk:"internet_access"` - Status types.String `tfsdk:"status"` - CreatedAt types.String `tfsdk:"created_at"` - UpdatedAt types.String `tfsdk:"updated_at"` - - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - State types.String `tfsdk:"state"` -} - -// NewProjectDataSource is a helper function to simplify the provider implementation. -func NewProjectDataSource() datasource.DataSource { - return &projectDataSource{} -} - -// projectDatasource is the data source implementation. -type projectDataSource struct { - client *iaas.APIClient -} - -func (d *projectDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Metadata returns the data source type name. -func (d *projectDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_iaas_project" -} - -// Schema defines the schema for the datasource. -func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Project details. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`\".", - "project_id": "STACKIT project ID.", - "area_id": "The area ID to which the project belongs to.", - "internet_access": "Specifies if the project has internet_access", - "status": "Specifies the status of the project.", - "created_at": "Date-time when the project was created.", - "updated_at": "Date-time when the project was last updated.", - } - resp.Schema = schema.Schema{ - MarkdownDescription: descriptions["main"], - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "area_id": schema.StringAttribute{ - Description: descriptions["area_id"], - Computed: true, - }, - "internet_access": schema.BoolAttribute{ - Description: descriptions["internet_access"], - Computed: true, - }, - // Deprecated: Will be removed in May 2026. Only kept to make the IaaS v1 -> v2 API migration non-breaking in the Terraform provider. - "state": schema.StringAttribute{ - DeprecationMessage: "Deprecated: Will be removed in May 2026. Use the `status` field instead.", - Description: descriptions["status"], - Computed: true, - }, - "status": schema.StringAttribute{ - Description: descriptions["status"], - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: descriptions["created_at"], - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: descriptions["updated_at"], - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DatasourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - - projectResp, err := d.client.GetProjectDetailsExecute(ctx, projectId) - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading project", - fmt.Sprintf("Project with ID %q does not exists.", projectId), - nil, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapDataSourceFields(projectResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Process API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "project read") -} - -func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) error { - if projectResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var projectId string - if model.ProjectId.ValueString() != "" { - projectId = model.ProjectId.ValueString() - } else if projectResp.Id != nil { - projectId = *projectResp.Id - } else { - return fmt.Errorf("project id is not present") - } - - model.Id = utils.BuildInternalTerraformId(projectId) - model.ProjectId = types.StringValue(projectId) - - var areaId basetypes.StringValue - if projectResp.AreaId != nil { - if projectResp.AreaId.String != nil { - areaId = types.StringPointerValue(projectResp.AreaId.String) - } else if projectResp.AreaId.StaticAreaID != nil { - areaId = types.StringValue(string(*projectResp.AreaId.StaticAreaID)) - } - } - - var createdAt basetypes.StringValue - if projectResp.CreatedAt != nil { - createdAtValue := *projectResp.CreatedAt - createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - - var updatedAt basetypes.StringValue - if projectResp.UpdatedAt != nil { - updatedAtValue := *projectResp.UpdatedAt - updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) - } - - model.AreaId = areaId - model.InternetAccess = types.BoolPointerValue(projectResp.InternetAccess) - model.State = types.StringPointerValue(projectResp.Status) - model.Status = types.StringPointerValue(projectResp.Status) - model.CreatedAt = createdAt - model.UpdatedAt = updatedAt - return nil -} diff --git a/stackit/internal/services/iaas/project/datasource_test.go b/stackit/internal/services/iaas/project/datasource_test.go deleted file mode 100644 index d2e57489..00000000 --- a/stackit/internal/services/iaas/project/datasource_test.go +++ /dev/null @@ -1,120 +0,0 @@ -package project - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -const ( - testTimestampValue = "2006-01-02T15:04:05Z" -) - -func testTimestamp() time.Time { - timestamp, _ := time.Parse(time.RFC3339, testTimestampValue) - return timestamp -} - -func TestMapDataSourceFields(t *testing.T) { - const projectId = "pid" - tests := []struct { - description string - state *DatasourceModel - input *iaas.Project - expected *DatasourceModel - isValid bool - }{ - { - description: "default_values", - state: &DatasourceModel{ - ProjectId: types.StringValue(projectId), - }, - input: &iaas.Project{ - Id: utils.Ptr(projectId), - }, - expected: &DatasourceModel{ - Id: types.StringValue(projectId), - ProjectId: types.StringValue(projectId), - }, - isValid: true, - }, - { - description: "simple_values", - state: &DatasourceModel{ - ProjectId: types.StringValue(projectId), - }, - input: &iaas.Project{ - AreaId: utils.Ptr(iaas.AreaId{String: utils.Ptr("aid")}), - CreatedAt: utils.Ptr(testTimestamp()), - InternetAccess: utils.Ptr(true), - Id: utils.Ptr(projectId), - Status: utils.Ptr("CREATED"), - UpdatedAt: utils.Ptr(testTimestamp()), - }, - expected: &DatasourceModel{ - Id: types.StringValue(projectId), - ProjectId: types.StringValue(projectId), - AreaId: types.StringValue("aid"), - InternetAccess: types.BoolValue(true), - State: types.StringValue("CREATED"), - Status: types.StringValue("CREATED"), - CreatedAt: types.StringValue(testTimestampValue), - UpdatedAt: types.StringValue(testTimestampValue), - }, - isValid: true, - }, - { - description: "static_area_id", - state: &DatasourceModel{ - ProjectId: types.StringValue(projectId), - }, - input: &iaas.Project{ - AreaId: utils.Ptr(iaas.AreaId{ - StaticAreaID: iaas.STATICAREAID_PUBLIC.Ptr(), - }), - Id: utils.Ptr(projectId), - }, - expected: &DatasourceModel{ - Id: types.StringValue(projectId), - ProjectId: types.StringValue(projectId), - AreaId: types.StringValue("PUBLIC"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - state: &DatasourceModel{}, - input: nil, - expected: &DatasourceModel{}, - isValid: false, - }, - { - description: "no_project_id_fail", - state: &DatasourceModel{}, - input: &iaas.Project{}, - expected: &DatasourceModel{}, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(tt.input, tt.state) - if !tt.isValid && err == nil { - t.Fatal("should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.expected, tt.state) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/publicip/datasource.go b/stackit/internal/services/iaas/publicip/datasource.go deleted file mode 100644 index 64b46425..00000000 --- a/stackit/internal/services/iaas/publicip/datasource.go +++ /dev/null @@ -1,158 +0,0 @@ -package publicip - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &publicIpDataSource{} -) - -// NewPublicIpDataSource is a helper function to simplify the provider implementation. -func NewPublicIpDataSource() datasource.DataSource { - return &publicIpDataSource{} -} - -// publicIpDataSource is the data source implementation. -type publicIpDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *publicIpDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_public_ip" -} - -func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (d *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Public IP resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the public IP is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "public_ip_id": schema.StringAttribute{ - Description: "The public IP ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "ip": schema.StringAttribute{ - Description: "The IP address.", - Computed: true, - }, - "network_interface_id": schema.StringAttribute{ - Description: "Associates the public IP with a network interface or a virtual IP (ID).", - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - publicIpId := model.PublicIpId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - - publicIpResp, err := d.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading public ip", - fmt.Sprintf("Public ip with ID %q does not exist in project %q.", publicIpId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, publicIpResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "public IP read") -} diff --git a/stackit/internal/services/iaas/publicip/resource.go b/stackit/internal/services/iaas/publicip/resource.go deleted file mode 100644 index aa8ac637..00000000 --- a/stackit/internal/services/iaas/publicip/resource.go +++ /dev/null @@ -1,453 +0,0 @@ -package publicip - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &publicIpResource{} - _ resource.ResourceWithConfigure = &publicIpResource{} - _ resource.ResourceWithImportState = &publicIpResource{} - _ resource.ResourceWithModifyPlan = &publicIpResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - PublicIpId types.String `tfsdk:"public_ip_id"` - Ip types.String `tfsdk:"ip"` - NetworkInterfaceId types.String `tfsdk:"network_interface_id"` - Labels types.Map `tfsdk:"labels"` -} - -// NewPublicIpResource is a helper function to simplify the provider implementation. -func NewPublicIpResource() resource.Resource { - return &publicIpResource{} -} - -// publicIpResource is the resource implementation. -type publicIpResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *publicIpResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_public_ip" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *publicIpResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *publicIpResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Public IP resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the public IP is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "public_ip_id": schema.StringAttribute{ - Description: "The public IP ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "ip": schema.StringAttribute{ - Description: "The IP address.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.IP(false), - }, - }, - "network_interface_id": schema.StringAttribute{ - Description: "Associates the public IP with a network interface or a virtual IP (ID). If you are using this resource with a Kubernetes Load Balancer or any other resource which associates a network interface implicitly, use the lifecycle `ignore_changes` property in this field to prevent unintentional removal of the network interface due to drift in the Terraform state", - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *publicIpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new public IP - - publicIp, err := r.client.CreatePublicIP(ctx, projectId, region).CreatePublicIPPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - ctx = tflog.SetField(ctx, "public_ip_id", *publicIp.Id) - - // Map response body to schema - err = mapFields(ctx, publicIp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Public IP created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - publicIpId := model.PublicIpId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - - publicIpResp, err := r.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, publicIpResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "public IP read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - publicIpId := model.PublicIpId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing public IP - updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, updatedPublicIp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "public IP updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - publicIpId := model.PublicIpId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - - // Delete existing publicIp - err := r.client.DeletePublicIP(ctx, projectId, region, publicIpId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting public IP", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "public IP deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,public_ip_id -func (r *publicIpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing public IP", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[public_ip_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "public_ip_id": idParts[2], - }) - - tflog.Info(ctx, "public IP state imported") -} - -func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model, region string) error { - if publicIpResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var publicIpId string - if model.PublicIpId.ValueString() != "" { - publicIpId = model.PublicIpId.ValueString() - } else if publicIpResp.Id != nil { - publicIpId = *publicIpResp.Id - } else { - return fmt.Errorf("public IP id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, publicIpId) - model.Region = types.StringValue(region) - - labels, err := iaasUtils.MapLabels(ctx, publicIpResp.Labels, model.Labels) - if err != nil { - return err - } - - model.PublicIpId = types.StringValue(publicIpId) - model.Ip = types.StringPointerValue(publicIpResp.Ip) - if publicIpResp.NetworkInterface != nil { - model.NetworkInterfaceId = types.StringPointerValue(publicIpResp.GetNetworkInterface()) - } else { - model.NetworkInterfaceId = types.StringNull() - } - model.Labels = labels - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreatePublicIPPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.CreatePublicIPPayload{ - Labels: &labels, - Ip: conversion.StringValueToPointer(model.Ip), - NetworkInterface: iaas.NewNullableString(conversion.StringValueToPointer(model.NetworkInterfaceId)), - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdatePublicIPPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.UpdatePublicIPPayload{ - Labels: &labels, - NetworkInterface: iaas.NewNullableString(conversion.StringValueToPointer(model.NetworkInterfaceId)), - }, nil -} diff --git a/stackit/internal/services/iaas/publicip/resource_test.go b/stackit/internal/services/iaas/publicip/resource_test.go deleted file mode 100644 index d1797897..00000000 --- a/stackit/internal/services/iaas/publicip/resource_test.go +++ /dev/null @@ -1,281 +0,0 @@ -package publicip - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.PublicIp - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - }, - input: &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - NetworkInterface: iaas.NewNullableString(nil), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,pipid"), - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Ip: types.StringNull(), - Labels: types.MapNull(types.StringType), - NetworkInterfaceId: types.StringNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - Ip: utils.Ptr("ip"), - Labels: &map[string]interface{}{ - "key": "value", - }, - NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,pipid"), - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Ip: types.StringValue("ip"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - NetworkInterfaceId: types.StringValue("interface"), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - input: &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,pipid"), - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Ip: types.StringNull(), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - NetworkInterfaceId: types.StringValue("interface"), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "network_interface_id_nil", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - }, - input: &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,pipid"), - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Ip: types.StringNull(), - Labels: types.MapNull(types.StringType), - NetworkInterfaceId: types.StringNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.PublicIp{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreatePublicIPPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Ip: types.StringValue("ip"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - NetworkInterfaceId: types.StringValue("interface"), - }, - &iaas.CreatePublicIPPayload{ - Ip: utils.Ptr("ip"), - Labels: &map[string]interface{}{ - "key": "value", - }, - NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), - }, - true, - }, - { - "network_interface_nil", - &Model{ - Ip: types.StringValue("ip"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - &iaas.CreatePublicIPPayload{ - Ip: utils.Ptr("ip"), - Labels: &map[string]interface{}{ - "key": "value", - }, - NetworkInterface: iaas.NewNullableString(nil), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdatePublicIPPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Ip: types.StringValue("ip"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - NetworkInterfaceId: types.StringValue("interface"), - }, - &iaas.UpdatePublicIPPayload{ - Labels: &map[string]interface{}{ - "key": "value", - }, - NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), - }, - true, - }, - { - "network_interface_nil", - &Model{ - Ip: types.StringValue("ip"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - &iaas.UpdatePublicIPPayload{ - Labels: &map[string]interface{}{ - "key": "value", - }, - NetworkInterface: iaas.NewNullableString(nil), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/publicipassociate/resource.go b/stackit/internal/services/iaas/publicipassociate/resource.go deleted file mode 100644 index 66028381..00000000 --- a/stackit/internal/services/iaas/publicipassociate/resource.go +++ /dev/null @@ -1,389 +0,0 @@ -package publicipassociate - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &publicIpAssociateResource{} - _ resource.ResourceWithConfigure = &publicIpAssociateResource{} - _ resource.ResourceWithImportState = &publicIpAssociateResource{} - _ resource.ResourceWithModifyPlan = &publicIpAssociateResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - PublicIpId types.String `tfsdk:"public_ip_id"` - Ip types.String `tfsdk:"ip"` - NetworkInterfaceId types.String `tfsdk:"network_interface_id"` -} - -// NewPublicIpAssociateResource is a helper function to simplify the provider implementation. -func NewPublicIpAssociateResource() resource.Resource { - return &publicIpAssociateResource{} -} - -// publicIpAssociateResource is the resource implementation. -type publicIpAssociateResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *publicIpAssociateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_public_ip_associate" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *publicIpAssociateResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *publicIpAssociateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - core.LogAndAddWarning(ctx, &resp.Diagnostics, "The `stackit_public_ip_associate` resource should not be used together with the `stackit_public_ip` resource for the same public IP or for the same network interface.", - "Using both resources together for the same public IP or network interface WILL lead to conflicts, as they both have control of the public IP and network interface association.") - - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Associates an existing public IP to a network interface. " + - "This is useful for situations where you have a pre-allocated public IP or unable to use the `stackit_public_ip` resource to create a new public IP. " + - "Must have a `region` specified in the provider configuration.", - "warning_message": "The `stackit_public_ip_associate` resource should not be used together with the `stackit_public_ip` resource for the same public IP or for the same network interface. \n" + - "Using both resources together for the same public IP or network interface WILL lead to conflicts, as they both have control of the public IP and network interface association.", - } - resp.Schema = schema.Schema{ - MarkdownDescription: fmt.Sprintf("%s\n\n!> %s", descriptions["main"], descriptions["warning_message"]), - Description: fmt.Sprintf("%s\n\n%s", descriptions["main"], descriptions["warning_message"]), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`public_ip_id`,`network_interface_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the public IP is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "public_ip_id": schema.StringAttribute{ - Description: "The public IP ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "ip": schema.StringAttribute{ - Description: "The IP address.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.IP(false), - }, - }, - "network_interface_id": schema.StringAttribute{ - Description: "The ID of the network interface (or virtual IP) to which the public IP should be attached to.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - publicIpId := model.PublicIpId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing public IP - updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(updatedPublicIp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "public IP associated to network interface") -} - -// Read refreshes the Terraform state with the latest data. -func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - publicIpId := model.PublicIpId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - publicIpResp, err := r.client.GetPublicIP(ctx, projectId, region, publicIpId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP association", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(publicIpResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP association", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "public IP associate read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *publicIpAssociateResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update is not supported, all fields require replace -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - publicIpId := model.PublicIpId.ValueString() - networkInterfaceId := model.NetworkInterfaceId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "public_ip_id", publicIpId) - ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId) - - payload := &iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(nil), - } - - _, err := r.client.UpdatePublicIP(ctx, projectId, region, publicIpId).UpdatePublicIPPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting public IP association", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "public IP association deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,public_ip_id -func (r *publicIpAssociateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing public IP associate", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[public_ip_id],[network_interface_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "public_ip_id": idParts[2], - "network_interface_id": idParts[3], - }) - - tflog.Info(ctx, "public IP state imported") -} - -func mapFields(publicIpResp *iaas.PublicIp, model *Model, region string) error { - if publicIpResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var publicIpId string - if model.PublicIpId.ValueString() != "" { - publicIpId = model.PublicIpId.ValueString() - } else if publicIpResp.Id != nil { - publicIpId = *publicIpResp.Id - } else { - return fmt.Errorf("public IP id not present") - } - - if publicIpResp.NetworkInterface != nil { - model.NetworkInterfaceId = types.StringPointerValue(publicIpResp.GetNetworkInterface()) - } else { - model.NetworkInterfaceId = types.StringNull() - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, publicIpId, model.NetworkInterfaceId.ValueString(), - ) - model.Region = types.StringValue(region) - model.PublicIpId = types.StringValue(publicIpId) - model.Ip = types.StringPointerValue(publicIpResp.Ip) - - return nil -} - -func toCreatePayload(model *Model) (*iaas.UpdatePublicIPPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(conversion.StringValueToPointer(model.NetworkInterfaceId)), - }, nil -} diff --git a/stackit/internal/services/iaas/publicipassociate/resource_test.go b/stackit/internal/services/iaas/publicipassociate/resource_test.go deleted file mode 100644 index f1c09f5a..00000000 --- a/stackit/internal/services/iaas/publicipassociate/resource_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package publicipassociate - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.PublicIp - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - NetworkInterfaceId: types.StringValue("nicid"), - }, - input: &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,pipid,nicid"), - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Ip: types.StringNull(), - NetworkInterfaceId: types.StringValue("nicid"), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - NetworkInterfaceId: types.StringValue("nicid"), - }, - input: &iaas.PublicIp{ - Id: utils.Ptr("pipid"), - Ip: utils.Ptr("ip"), - NetworkInterface: iaas.NewNullableString(utils.Ptr("nicid")), - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,pipid,nicid"), - ProjectId: types.StringValue("pid"), - PublicIpId: types.StringValue("pipid"), - Ip: types.StringValue("ip"), - NetworkInterfaceId: types.StringValue("nicid"), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.PublicIp{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdatePublicIPPayload - isValid bool - }{ - { - "default_ok", - &Model{ - NetworkInterfaceId: types.StringValue("interface"), - }, - &iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/publicipranges/datasource.go b/stackit/internal/services/iaas/publicipranges/datasource.go deleted file mode 100644 index 1d3709a9..00000000 --- a/stackit/internal/services/iaas/publicipranges/datasource.go +++ /dev/null @@ -1,220 +0,0 @@ -package publicipranges - -import ( - "context" - "fmt" - "net/http" - "sort" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &publicIpRangesDataSource{} -) - -// NewPublicIpRangesDataSource is a helper function to simplify the provider implementation. -func NewPublicIpRangesDataSource() datasource.DataSource { - return &publicIpRangesDataSource{} -} - -// publicIpRangesDataSource is the data source implementation. -type publicIpRangesDataSource struct { - client *iaas.APIClient -} - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - PublicIpRanges types.List `tfsdk:"public_ip_ranges"` - CidrList types.List `tfsdk:"cidr_list"` -} - -var publicIpRangesTypes = map[string]attr.Type{ - "cidr": types.StringType, -} - -// Metadata returns the data source type name. -func (d *publicIpRangesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_public_ip_ranges" -} - -func (d *publicIpRangesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (d *publicIpRangesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "A list of all public IP ranges that STACKIT uses." - - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It takes the values of \"`public_ip_ranges.*.cidr`\".", - Computed: true, - Optional: false, - }, - "public_ip_ranges": schema.ListNestedAttribute{ - Description: "A list of all public IP ranges.", - Computed: true, - Optional: false, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - validate.CIDR(), - ), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "cidr": schema.StringAttribute{ - Description: "Classless Inter-Domain Routing (CIDR)", - Computed: true, - }, - }, - }, - }, - "cidr_list": schema.ListAttribute{ - Description: "A list of IP range strings (CIDRs) extracted from the public_ip_ranges for easy consumption.", - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *publicIpRangesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - publicIpRangeResp, err := d.client.ListPublicIPRangesExecute(ctx) - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading public ip ranges", - "Public ip ranges cannot be found", - map[int]string{ - http.StatusForbidden: "Forbidden access", - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, publicIpRangeResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP ranges", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "read public IP ranges") -} - -func mapFields(ctx context.Context, publicIpRangeResp *iaas.PublicNetworkListResponse, model *Model) error { - if publicIpRangeResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - err := mapPublicIpRanges(ctx, publicIpRangeResp.Items, model) - if err != nil { - return fmt.Errorf("error mapping public IP ranges: %w", err) - } - return nil -} - -// mapPublicIpRanges map the response publicIpRanges to the model -func mapPublicIpRanges(ctx context.Context, publicIpRanges *[]iaas.PublicNetwork, model *Model) error { - if publicIpRanges == nil { - return fmt.Errorf("publicIpRanges input is nil") - } - if len(*publicIpRanges) == 0 { - model.PublicIpRanges = types.ListNull(types.ObjectType{AttrTypes: publicIpRangesTypes}) - model.CidrList = types.ListNull(types.StringType) - return nil - } - - var apiIpRanges []string - for _, ipRange := range *publicIpRanges { - if ipRange.Cidr != nil && *ipRange.Cidr != "" { - apiIpRanges = append(apiIpRanges, *ipRange.Cidr) - } - } - - // Sort to prevent unnecessary recreation of dependent resources due to order changes. - sort.Strings(apiIpRanges) - - model.Id = utils.BuildInternalTerraformId(apiIpRanges...) - - var ipRangesList []attr.Value - for _, cidr := range apiIpRanges { - ipRangeValues := map[string]attr.Value{ - "cidr": types.StringValue(cidr), - } - ipRangeObject, diag := types.ObjectValue(publicIpRangesTypes, ipRangeValues) - if diag.HasError() { - return core.DiagsToError(diag) - } - ipRangesList = append(ipRangesList, ipRangeObject) - } - - ipRangesTF, diags := types.ListValue( - types.ObjectType{AttrTypes: publicIpRangesTypes}, - ipRangesList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - model.PublicIpRanges = ipRangesTF - - cidrListTF, diags := types.ListValueFrom(ctx, types.StringType, apiIpRanges) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.CidrList = cidrListTF - - return nil -} diff --git a/stackit/internal/services/iaas/publicipranges/datasource_test.go b/stackit/internal/services/iaas/publicipranges/datasource_test.go deleted file mode 100644 index 535df5f7..00000000 --- a/stackit/internal/services/iaas/publicipranges/datasource_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package publicipranges - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - coreUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func TestMapPublicIpRanges(t *testing.T) { - ctx := context.Background() - - tests := []struct { - name string - input *[]iaas.PublicNetwork - expected Model - isValid bool - }{ - { - name: "nil input should return error", - input: nil, - isValid: false, - }, - { - name: "empty input should return nulls", - input: &[]iaas.PublicNetwork{}, - expected: Model{ - PublicIpRanges: types.ListNull(types.ObjectType{AttrTypes: publicIpRangesTypes}), - CidrList: types.ListNull(types.StringType), - }, - isValid: true, - }, - { - name: "valid cidr entries", - input: &[]iaas.PublicNetwork{ - {Cidr: coreUtils.Ptr("192.168.0.0/24")}, - {Cidr: coreUtils.Ptr("192.168.1.0/24")}, - }, - expected: func() Model { - cidrs := []string{"192.168.0.0/24", "192.168.1.0/24"} - ipRangesList := make([]attr.Value, 0, len(cidrs)) - for _, cidr := range cidrs { - ipRange, _ := types.ObjectValue(publicIpRangesTypes, map[string]attr.Value{ - "cidr": types.StringValue(cidr), - }) - ipRangesList = append(ipRangesList, ipRange) - } - ipRangesVal, _ := types.ListValue(types.ObjectType{AttrTypes: publicIpRangesTypes}, ipRangesList) - cidrListVal, _ := types.ListValueFrom(ctx, types.StringType, cidrs) - - return Model{ - PublicIpRanges: ipRangesVal, - CidrList: cidrListVal, - Id: utils.BuildInternalTerraformId(cidrs...), - } - }(), - isValid: true, - }, - { - name: "filter out empty CIDRs", - input: &[]iaas.PublicNetwork{ - {Cidr: coreUtils.Ptr("")}, - {Cidr: nil}, - {Cidr: coreUtils.Ptr("10.0.0.0/8")}, - }, - expected: func() Model { - cidrs := []string{"10.0.0.0/8"} - ipRange, _ := types.ObjectValue(publicIpRangesTypes, map[string]attr.Value{ - "cidr": types.StringValue("10.0.0.0/8"), - }) - ipRangesVal, _ := types.ListValue(types.ObjectType{AttrTypes: publicIpRangesTypes}, []attr.Value{ipRange}) - cidrListVal, _ := types.ListValueFrom(ctx, types.StringType, cidrs) - return Model{ - PublicIpRanges: ipRangesVal, - CidrList: cidrListVal, - Id: utils.BuildInternalTerraformId(cidrs...), - } - }(), - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var model Model - err := mapPublicIpRanges(ctx, tt.input, &model) - - if !tt.isValid { - if err == nil { - t.Fatalf("Expected error but got nil") - } - return - } else if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - if diff := cmp.Diff(tt.expected.Id, model.Id); diff != "" { - t.Errorf("ID does not match:\n%s", diff) - } - - if diff := cmp.Diff(tt.expected.CidrList, model.CidrList); diff != "" { - t.Errorf("cidr_list does not match:\n%s", diff) - } - - if diff := cmp.Diff(tt.expected.PublicIpRanges, model.PublicIpRanges); diff != "" { - t.Errorf("public_ip_ranges does not match:\n%s", diff) - } - }) - } -} diff --git a/stackit/internal/services/iaas/securitygroup/datasource.go b/stackit/internal/services/iaas/securitygroup/datasource.go deleted file mode 100644 index d2b87e79..00000000 --- a/stackit/internal/services/iaas/securitygroup/datasource.go +++ /dev/null @@ -1,158 +0,0 @@ -package securitygroup - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &securityGroupDataSource{} -) - -// NewSecurityGroupDataSource is a helper function to simplify the provider implementation. -func NewSecurityGroupDataSource() datasource.DataSource { - return &securityGroupDataSource{} -} - -// securityGroupDataSource is the data source implementation. -type securityGroupDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *securityGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_security_group" -} - -func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (d *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Security group datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the security group is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "security_group_id": schema.StringAttribute{ - Description: "The security group ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the security group.", - Computed: true, - }, - "description": schema.StringAttribute{ - Description: "The description of the security group.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - "stateful": schema.BoolAttribute{ - Description: "Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server.", - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - - securityGroupResp, err := d.client.GetSecurityGroup(ctx, projectId, region, securityGroupId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading security group", - fmt.Sprintf("Security group with ID %q does not exist in project %q.", securityGroupId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, securityGroupResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "security group read") -} diff --git a/stackit/internal/services/iaas/securitygroup/resource.go b/stackit/internal/services/iaas/securitygroup/resource.go deleted file mode 100644 index 07b510ac..00000000 --- a/stackit/internal/services/iaas/securitygroup/resource.go +++ /dev/null @@ -1,472 +0,0 @@ -package securitygroup - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &securityGroupResource{} - _ resource.ResourceWithConfigure = &securityGroupResource{} - _ resource.ResourceWithImportState = &securityGroupResource{} - _ resource.ResourceWithModifyPlan = &securityGroupResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - SecurityGroupId types.String `tfsdk:"security_group_id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Labels types.Map `tfsdk:"labels"` - Stateful types.Bool `tfsdk:"stateful"` -} - -// NewSecurityGroupResource is a helper function to simplify the provider implementation. -func NewSecurityGroupResource() resource.Resource { - return &securityGroupResource{} -} - -// securityGroupResource is the resource implementation. -type securityGroupResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *securityGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_security_group" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *securityGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *securityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Security group resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the security group is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "security_group_id": schema.StringAttribute{ - Description: "The security group ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the security group.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "description": schema.StringAttribute{ - Description: "The description of the security group.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(127), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - "stateful": schema.BoolAttribute{ - Description: "Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - boolplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new security group - - securityGroup, err := r.client.CreateSecurityGroup(ctx, projectId, region).CreateSecurityGroupPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - securityGroupId := *securityGroup.Id - - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - - // Map response body to schema - err = mapFields(ctx, securityGroup, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Security group created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_id", securityGroupId) - - securityGroupResp, err := r.client.GetSecurityGroup(ctx, projectId, region, securityGroupId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, securityGroupResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "security group read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing security group - updatedSecurityGroup, err := r.client.UpdateSecurityGroup(ctx, projectId, region, securityGroupId).UpdateSecurityGroupPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, updatedSecurityGroup, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "security group updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - - // Delete existing security group - err := r.client.DeleteSecurityGroup(ctx, projectId, region, securityGroupId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "security group deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,security_group_id -func (r *securityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing security group", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[security_group_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "security_group_id": idParts[2], - }) - - tflog.Info(ctx, "security group state imported") -} - -func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model *Model, region string) error { - if securityGroupResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var securityGroupId string - if model.SecurityGroupId.ValueString() != "" { - securityGroupId = model.SecurityGroupId.ValueString() - } else if securityGroupResp.Id != nil { - securityGroupId = *securityGroupResp.Id - } else { - return fmt.Errorf("security group id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, securityGroupId) - model.Region = types.StringValue(region) - - labels, err := iaasUtils.MapLabels(ctx, securityGroupResp.Labels, model.Labels) - if err != nil { - return err - } - - model.SecurityGroupId = types.StringValue(securityGroupId) - model.Name = types.StringPointerValue(securityGroupResp.Name) - model.Description = types.StringPointerValue(securityGroupResp.Description) - model.Stateful = types.BoolPointerValue(securityGroupResp.Stateful) - model.Labels = labels - - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateSecurityGroupPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.CreateSecurityGroupPayload{ - Stateful: conversion.BoolValueToPointer(model.Stateful), - Description: conversion.StringValueToPointer(model.Description), - Labels: &labels, - Name: conversion.StringValueToPointer(model.Name), - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateSecurityGroupPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.UpdateSecurityGroupPayload{ - Description: conversion.StringValueToPointer(model.Description), - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - }, nil -} diff --git a/stackit/internal/services/iaas/securitygroup/resource_test.go b/stackit/internal/services/iaas/securitygroup/resource_test.go deleted file mode 100644 index 37498656..00000000 --- a/stackit/internal/services/iaas/securitygroup/resource_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package securitygroup - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.SecurityGroup - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - }, - input: &iaas.SecurityGroup{ - Id: utils.Ptr("sgid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,sgid"), - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - Name: types.StringNull(), - Labels: types.MapNull(types.StringType), - Description: types.StringNull(), - Stateful: types.BoolNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.SecurityGroup{ - Id: utils.Ptr("sgid"), - Name: utils.Ptr("name"), - Stateful: utils.Ptr(true), - Labels: &map[string]interface{}{ - "key": "value", - }, - Description: utils.Ptr("desc"), - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,sgid"), - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - Name: types.StringValue("name"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Description: types.StringValue("desc"), - Stateful: types.BoolValue(true), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - }, - input: &iaas.SecurityGroup{ - Id: utils.Ptr("sgid"), - Labels: &map[string]interface{}{}, - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,sgid"), - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - Name: types.StringNull(), - Labels: types.MapNull(types.StringType), - Description: types.StringNull(), - Stateful: types.BoolNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.SecurityGroup{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateSecurityGroupPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - Stateful: types.BoolValue(true), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Description: types.StringValue("desc"), - }, - &iaas.CreateSecurityGroupPayload{ - Name: utils.Ptr("name"), - Stateful: utils.Ptr(true), - Labels: &map[string]interface{}{ - "key": "value", - }, - Description: utils.Ptr("desc"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdateSecurityGroupPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Description: types.StringValue("desc"), - }, - &iaas.UpdateSecurityGroupPayload{ - Name: utils.Ptr("name"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Description: utils.Ptr("desc"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/securitygrouprule/datasource.go b/stackit/internal/services/iaas/securitygrouprule/datasource.go deleted file mode 100644 index fb675966..00000000 --- a/stackit/internal/services/iaas/securitygrouprule/datasource.go +++ /dev/null @@ -1,214 +0,0 @@ -package securitygrouprule - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &securityGroupRuleDataSource{} -) - -// NewSecurityGroupRuleDataSource is a helper function to simplify the provider implementation. -func NewSecurityGroupRuleDataSource() datasource.DataSource { - return &securityGroupRuleDataSource{} -} - -// securityGroupRuleDataSource is the data source implementation. -type securityGroupRuleDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *securityGroupRuleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_security_group_rule" -} - -func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (d *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - directionOptions := []string{"ingress", "egress"} - description := "Security group datasource schema. Must have a `region` specified in the provider configuration." - - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the security group rule is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "security_group_id": schema.StringAttribute{ - Description: "The security group ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "security_group_rule_id": schema.StringAttribute{ - Description: "The security group rule ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "direction": schema.StringAttribute{ - Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.FormatPossibleValues(directionOptions...), - Computed: true, - }, - "description": schema.StringAttribute{ - Description: "The description of the security group rule.", - Computed: true, - }, - "ether_type": schema.StringAttribute{ - Description: "The ethertype which the rule should match.", - Computed: true, - }, - "icmp_parameters": schema.SingleNestedAttribute{ - Description: "ICMP Parameters.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "code": schema.Int64Attribute{ - Description: "ICMP code. Can be set if the protocol is ICMP.", - Computed: true, - }, - "type": schema.Int64Attribute{ - Description: "ICMP type. Can be set if the protocol is ICMP.", - Computed: true, - }, - }, - }, - "ip_range": schema.StringAttribute{ - Description: "The remote IP range which the rule should match.", - Computed: true, - }, - "port_range": schema.SingleNestedAttribute{ - Description: "The range of ports.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "max": schema.Int64Attribute{ - Description: "The maximum port number. Should be greater or equal to the minimum.", - Computed: true, - }, - "min": schema.Int64Attribute{ - Description: "The minimum port number. Should be less or equal to the minimum.", - Computed: true, - }, - }, - }, - "protocol": schema.SingleNestedAttribute{ - Description: "The internet protocol which the rule should match.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "The protocol name which the rule should match.", - Computed: true, - }, - "number": schema.Int64Attribute{ - Description: "The protocol number which the rule should match.", - Computed: true, - }, - }, - }, - "remote_security_group_id": schema.StringAttribute{ - Description: "The remote security group which the rule should match.", - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - securityGroupRuleId := model.SecurityGroupRuleId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId) - - securityGroupRuleResp, err := d.client.GetSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading security group rule", - fmt.Sprintf("Security group rule with ID %q or security group with ID %q does not exist in project %q.", securityGroupRuleId, securityGroupId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(securityGroupRuleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "security group rule read") -} diff --git a/stackit/internal/services/iaas/securitygrouprule/planmodifier.go b/stackit/internal/services/iaas/securitygrouprule/planmodifier.go deleted file mode 100644 index 23d879f9..00000000 --- a/stackit/internal/services/iaas/securitygrouprule/planmodifier.go +++ /dev/null @@ -1,93 +0,0 @@ -package securitygrouprule - -import ( - "context" - "slices" - - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" -) - -// UseNullForUnknownBasedOnProtocolModifier returns a plan modifier that sets a null -// value into the planned value, based on the value of the protocol.name attribute. -// -// To prevent Terraform errors, the framework automatically sets unconfigured -// and Computed attributes to an unknown value "(known after apply)" on update. -// To prevent always showing "(known after apply)" on update for an attribute, e.g. port_range, which never changes in case the protocol is a specific one, -// we set the value to null. -// Examples: port_range is only computed if protocol is not icmp and icmp_parameters is only computed if protocol is icmp -func UseNullForUnknownBasedOnProtocolModifier() planmodifier.Object { - return useNullForUnknownBasedOnProtocolModifier{} -} - -// useNullForUnknownBasedOnProtocolModifier implements the plan modifier. -type useNullForUnknownBasedOnProtocolModifier struct{} - -func (m useNullForUnknownBasedOnProtocolModifier) Description(_ context.Context) string { - return "If protocol.name attribute is set and the value corresponds to an icmp protocol, the value of this attribute in state will be set to null." -} - -// MarkdownDescription returns a markdown description of the plan modifier. -func (m useNullForUnknownBasedOnProtocolModifier) MarkdownDescription(_ context.Context) string { - return "Once set, the value of this attribute in state will be set to null if protocol.name attribute is set and the value corresponds to an icmp protocol." -} - -// PlanModifyBool implements the plan modification logic. -func (m useNullForUnknownBasedOnProtocolModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { // nolint:gocritic // function signature required by Terraform - // Check if the resource is being created. - if req.State.Raw.IsNull() { - return - } - - // Do nothing if there is a known planned value. - if !req.PlanValue.IsUnknown() { - return - } - - // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. - if req.ConfigValue.IsUnknown() { - return - } - - // If there is an unknown configuration value, check if the value of protocol.name attribute corresponds to an icmp protocol. If it does, set the attribute value to null - var model Model - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - // If protocol is not configured, return without error. - if model.Protocol.IsNull() || model.Protocol.IsUnknown() { - return - } - - protocol := &protocolModel{} - diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - protocolName := conversion.StringValueToPointer(protocol.Name) - - if protocolName == nil { - return - } - - if slices.Contains(icmpProtocols, *protocolName) { - if model.PortRange.IsUnknown() { - resp.PlanValue = types.ObjectNull(portRangeTypes) - return - } - } else { - if model.IcmpParameters.IsUnknown() { - resp.PlanValue = types.ObjectNull(icmpParametersTypes) - return - } - } - - // use state for unknown if the value was not set to null - resp.PlanValue = req.StateValue -} diff --git a/stackit/internal/services/iaas/securitygrouprule/resource.go b/stackit/internal/services/iaas/securitygrouprule/resource.go deleted file mode 100644 index ab075b47..00000000 --- a/stackit/internal/services/iaas/securitygrouprule/resource.go +++ /dev/null @@ -1,804 +0,0 @@ -package securitygrouprule - -import ( - "context" - "fmt" - "net/http" - "regexp" - "slices" - "strings" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "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/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &securityGroupRuleResource{} - _ resource.ResourceWithConfigure = &securityGroupRuleResource{} - _ resource.ResourceWithImportState = &securityGroupRuleResource{} - _ resource.ResourceWithModifyPlan = &securityGroupRuleResource{} - - icmpProtocols = []string{"icmp", "ipv6-icmp"} - protocolsPossibleValues = []string{ - "ah", "dccp", "egp", "esp", "gre", "icmp", "igmp", "ipip", "ipv6-encap", "ipv6-frag", "ipv6-icmp", - "ipv6-nonxt", "ipv6-opts", "ipv6-route", "ospf", "pgm", "rsvp", "sctp", "tcp", "udp", "udplite", "vrrp", - } -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - SecurityGroupId types.String `tfsdk:"security_group_id"` - SecurityGroupRuleId types.String `tfsdk:"security_group_rule_id"` - Direction types.String `tfsdk:"direction"` - Description types.String `tfsdk:"description"` - EtherType types.String `tfsdk:"ether_type"` - IcmpParameters types.Object `tfsdk:"icmp_parameters"` - IpRange types.String `tfsdk:"ip_range"` - PortRange types.Object `tfsdk:"port_range"` - Protocol types.Object `tfsdk:"protocol"` - RemoteSecurityGroupId types.String `tfsdk:"remote_security_group_id"` -} - -type icmpParametersModel struct { - Code types.Int64 `tfsdk:"code"` - Type types.Int64 `tfsdk:"type"` -} - -// Types corresponding to icmpParameters -var icmpParametersTypes = map[string]attr.Type{ - "code": basetypes.Int64Type{}, - "type": basetypes.Int64Type{}, -} - -type portRangeModel struct { - Max types.Int64 `tfsdk:"max"` - Min types.Int64 `tfsdk:"min"` -} - -// Types corresponding to portRange -var portRangeTypes = map[string]attr.Type{ - "max": basetypes.Int64Type{}, - "min": basetypes.Int64Type{}, -} - -type protocolModel struct { - Name types.String `tfsdk:"name"` - Number types.Int64 `tfsdk:"number"` -} - -// Types corresponding to protocol -var protocolTypes = map[string]attr.Type{ - "name": basetypes.StringType{}, - "number": basetypes.Int64Type{}, -} - -// NewSecurityGroupRuleResource is a helper function to simplify the provider implementation. -func NewSecurityGroupRuleResource() resource.Resource { - return &securityGroupRuleResource{} -} - -// securityGroupRuleResource is the resource implementation. -type securityGroupRuleResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *securityGroupRuleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_security_group_rule" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *securityGroupRuleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -func (r *securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var model Model - - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - - if resp.Diagnostics.HasError() { - return - } - - // If protocol is not configured, return without error. - if model.Protocol.IsNull() || model.Protocol.IsUnknown() { - return - } - - protocol := &protocolModel{} - diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - protocolName := conversion.StringValueToPointer(protocol.Name) - - if protocolName == nil { - return - } - - if slices.Contains(icmpProtocols, *protocolName) { - if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) { - resp.Diagnostics.AddAttributeError( - path.Root("port_range"), - "Conflicting attribute configuration", - "`port_range` attribute can't be provided if `protocol.name` is set to `icmp` or `ipv6-icmp`", - ) - } - } else { - if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) { - resp.Diagnostics.AddAttributeError( - path.Root("icmp_parameters"), - "Conflicting attribute configuration", - "`icmp_parameters` attribute can't be provided if `protocol.name` is not `icmp` or `ipv6-icmp`", - ) - } - } -} - -// Schema defines the schema for the resource. -func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - directionOptions := []string{"ingress", "egress"} - description := "Security group rule resource schema. Must have a `region` specified in the provider configuration." - - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`security_group_id`,`security_group_rule_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the security group rule is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "security_group_id": schema.StringAttribute{ - Description: "The security group ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "security_group_rule_id": schema.StringAttribute{ - Description: "The security group rule ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "description": schema.StringAttribute{ - Description: "The rule description.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplaceIfConfigured(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtMost(127), - }, - }, - "direction": schema.StringAttribute{ - Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.FormatPossibleValues(directionOptions...), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "ether_type": schema.StringAttribute{ - Description: "The ethertype which the rule should match.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplaceIfConfigured(), - }, - }, - "icmp_parameters": schema.SingleNestedAttribute{ - Description: "ICMP Parameters. These parameters should only be provided if the protocol is ICMP.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - UseNullForUnknownBasedOnProtocolModifier(), - objectplanmodifier.RequiresReplaceIfConfigured(), - }, - Attributes: map[string]schema.Attribute{ - "code": schema.Int64Attribute{ - Description: "ICMP code. Can be set if the protocol is ICMP.", - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - int64validator.AtMost(255), - }, - }, - "type": schema.Int64Attribute{ - Description: "ICMP type. Can be set if the protocol is ICMP.", - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - int64validator.AtMost(255), - }, - }, - }, - }, - "ip_range": schema.StringAttribute{ - Description: "The remote IP range which the rule should match.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile(`^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/(3[0-2]|2[0-9]|1[0-9]|[0-9]))$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/((1(1[0-9]|2[0-8]))|([0-9][0-9])|([0-9])))?$`), - "must match expression"), - }, - }, - "port_range": schema.SingleNestedAttribute{ - Description: "The range of ports. This should only be provided if the protocol is not ICMP.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.RequiresReplaceIfConfigured(), - UseNullForUnknownBasedOnProtocolModifier(), - }, - Attributes: map[string]schema.Attribute{ - "max": schema.Int64Attribute{ - Description: "The maximum port number. Should be greater or equal to the minimum.", - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - int64validator.AtMost(65535), - }, - }, - "min": schema.Int64Attribute{ - Description: "The minimum port number. Should be less or equal to the maximum.", - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - int64validator.AtMost(65535), - }, - }, - }, - }, - "protocol": schema.SingleNestedAttribute{ - Description: "The internet protocol which the rule should match.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.RequiresReplaceIfConfigured(), - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: fmt.Sprintf("The protocol name which the rule should match. Either `name` or `number` must be provided. %s", utils.FormatPossibleValues(protocolsPossibleValues...)), - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.AtLeastOneOf( - path.MatchRoot("protocol").AtName("number"), - ), - stringvalidator.ConflictsWith( - path.MatchRoot("protocol").AtName("number"), - ), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplaceIfConfigured(), - }, - }, - "number": schema.Int64Attribute{ - Description: "The protocol number which the rule should match. Either `name` or `number` must be provided.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - int64planmodifier.RequiresReplaceIfConfigured(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(0), - int64validator.AtMost(255), - }, - }, - }, - }, - "remote_security_group_id": schema.StringAttribute{ - Description: "The remote security group which the rule should match.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - - var icmpParameters *icmpParametersModel - if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) { - icmpParameters = &icmpParametersModel{} - diags = model.IcmpParameters.As(ctx, icmpParameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var portRange *portRangeModel - if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) { - portRange = &portRangeModel{} - diags = model.PortRange.As(ctx, portRange, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var protocol *protocolModel - if !(model.Protocol.IsNull() || model.Protocol.IsUnknown()) { - protocol = &protocolModel{} - diags = model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, icmpParameters, portRange, protocol) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new security group rule - securityGroupRule, err := r.client.CreateSecurityGroupRule(ctx, projectId, region, securityGroupId).CreateSecurityGroupRulePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - ctx = tflog.SetField(ctx, "security_group_rule_id", *securityGroupRule.Id) - - // Map response body to schema - err = mapFields(securityGroupRule, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Security group rule created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - securityGroupRuleId := model.SecurityGroupRuleId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId) - - securityGroupRuleResp, err := r.client.GetSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(securityGroupRuleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "security group rule read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *securityGroupRuleResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group rule", "Security group rule can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - securityGroupId := model.SecurityGroupId.ValueString() - securityGroupRuleId := model.SecurityGroupRuleId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) - ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId) - - // Delete existing security group rule - err := r.client.DeleteSecurityGroupRule(ctx, projectId, region, securityGroupId, securityGroupRuleId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group rule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "security group rule deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,security_group_id, security_group_rule_id -func (r *securityGroupRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing security group rule", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[security_group_id],[security_group_rule_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "security_group_id": idParts[2], - "security_group_rule_id": idParts[3], - }) - - tflog.Info(ctx, "security group rule state imported") -} - -func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model, region string) error { - if securityGroupRuleResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var securityGroupRuleId string - if model.SecurityGroupRuleId.ValueString() != "" { - securityGroupRuleId = model.SecurityGroupRuleId.ValueString() - } else if securityGroupRuleResp.Id != nil { - securityGroupRuleId = *securityGroupRuleResp.Id - } else { - return fmt.Errorf("security group rule id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.SecurityGroupId.ValueString(), securityGroupRuleId) - model.Region = types.StringValue(region) - model.SecurityGroupRuleId = types.StringValue(securityGroupRuleId) - model.Direction = types.StringPointerValue(securityGroupRuleResp.Direction) - model.Description = types.StringPointerValue(securityGroupRuleResp.Description) - model.EtherType = types.StringPointerValue(securityGroupRuleResp.Ethertype) - model.IpRange = types.StringPointerValue(securityGroupRuleResp.IpRange) - model.RemoteSecurityGroupId = types.StringPointerValue(securityGroupRuleResp.RemoteSecurityGroupId) - - err := mapIcmpParameters(securityGroupRuleResp, model) - if err != nil { - return fmt.Errorf("map icmp_parameters: %w", err) - } - err = mapPortRange(securityGroupRuleResp, model) - if err != nil { - return fmt.Errorf("map port_range: %w", err) - } - err = mapProtocol(securityGroupRuleResp, model) - if err != nil { - return fmt.Errorf("map protocol: %w", err) - } - - return nil -} - -func mapIcmpParameters(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error { - if securityGroupRuleResp.IcmpParameters == nil { - m.IcmpParameters = types.ObjectNull(icmpParametersTypes) - return nil - } - - icmpParametersValues := map[string]attr.Value{ - "type": types.Int64Value(*securityGroupRuleResp.IcmpParameters.Type), - "code": types.Int64Value(*securityGroupRuleResp.IcmpParameters.Code), - } - - icmpParametersObject, diags := types.ObjectValue(icmpParametersTypes, icmpParametersValues) - if diags.HasError() { - return fmt.Errorf("create icmpParameters object: %w", core.DiagsToError(diags)) - } - m.IcmpParameters = icmpParametersObject - return nil -} - -func mapPortRange(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error { - if securityGroupRuleResp.PortRange == nil { - m.PortRange = types.ObjectNull(portRangeTypes) - return nil - } - - portRangeMax := types.Int64Null() - portRangeMin := types.Int64Null() - - if securityGroupRuleResp.PortRange.Max != nil { - portRangeMax = types.Int64Value(*securityGroupRuleResp.PortRange.Max) - } - - if securityGroupRuleResp.PortRange.Min != nil { - portRangeMin = types.Int64Value(*securityGroupRuleResp.PortRange.Min) - } - - portRangeValues := map[string]attr.Value{ - "max": portRangeMax, - "min": portRangeMin, - } - - portRangeObject, diags := types.ObjectValue(portRangeTypes, portRangeValues) - if diags.HasError() { - return fmt.Errorf("create portRange object: %w", core.DiagsToError(diags)) - } - m.PortRange = portRangeObject - return nil -} - -func mapProtocol(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error { - if securityGroupRuleResp.Protocol == nil { - m.Protocol = types.ObjectNull(protocolTypes) - return nil - } - - protocolNumberValue := types.Int64Null() - if securityGroupRuleResp.Protocol.Number != nil { - protocolNumberValue = types.Int64Value(*securityGroupRuleResp.Protocol.Number) - } - - protocolNameValue := types.StringNull() - if securityGroupRuleResp.Protocol.Name != nil { - protocolNameValue = types.StringValue(*securityGroupRuleResp.Protocol.Name) - } - - protocolValues := map[string]attr.Value{ - "name": protocolNameValue, - "number": protocolNumberValue, - } - protocolObject, diags := types.ObjectValue(protocolTypes, protocolValues) - if diags.HasError() { - return fmt.Errorf("create protocol object: %w", core.DiagsToError(diags)) - } - m.Protocol = protocolObject - return nil -} - -func toCreatePayload(model *Model, icmpParameters *icmpParametersModel, portRange *portRangeModel, protocol *protocolModel) (*iaas.CreateSecurityGroupRulePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payloadIcmpParameters, err := toIcmpParametersPayload(icmpParameters) - if err != nil { - return nil, fmt.Errorf("converting icmp parameters: %w", err) - } - - payloadPortRange, err := toPortRangePayload(portRange) - if err != nil { - return nil, fmt.Errorf("converting port range: %w", err) - } - - payloadProtocol, err := toProtocolPayload(protocol) - if err != nil { - return nil, fmt.Errorf("converting protocol: %w", err) - } - - return &iaas.CreateSecurityGroupRulePayload{ - Description: conversion.StringValueToPointer(model.Description), - Direction: conversion.StringValueToPointer(model.Direction), - Ethertype: conversion.StringValueToPointer(model.EtherType), - IpRange: conversion.StringValueToPointer(model.IpRange), - RemoteSecurityGroupId: conversion.StringValueToPointer(model.RemoteSecurityGroupId), - IcmpParameters: payloadIcmpParameters, - PortRange: payloadPortRange, - Protocol: payloadProtocol, - }, nil -} - -func toIcmpParametersPayload(icmpParameters *icmpParametersModel) (*iaas.ICMPParameters, error) { - if icmpParameters == nil { - return nil, nil - } - payloadParams := &iaas.ICMPParameters{} - - payloadParams.Code = conversion.Int64ValueToPointer(icmpParameters.Code) - payloadParams.Type = conversion.Int64ValueToPointer(icmpParameters.Type) - - return payloadParams, nil -} - -func toPortRangePayload(portRange *portRangeModel) (*iaas.PortRange, error) { - if portRange == nil { - return nil, nil - } - payloadPortRange := &iaas.PortRange{} - - payloadPortRange.Max = conversion.Int64ValueToPointer(portRange.Max) - payloadPortRange.Min = conversion.Int64ValueToPointer(portRange.Min) - - return payloadPortRange, nil -} - -func toProtocolPayload(protocol *protocolModel) (*iaas.CreateProtocol, error) { - if protocol == nil { - return nil, nil - } - payloadProtocol := &iaas.CreateProtocol{} - - payloadProtocol.String = conversion.StringValueToPointer(protocol.Name) - payloadProtocol.Int64 = conversion.Int64ValueToPointer(protocol.Number) - - return payloadProtocol, nil -} diff --git a/stackit/internal/services/iaas/securitygrouprule/resource_test.go b/stackit/internal/services/iaas/securitygrouprule/resource_test.go deleted file mode 100644 index dbf46f59..00000000 --- a/stackit/internal/services/iaas/securitygrouprule/resource_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package securitygrouprule - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -var fixtureModelIcmpParameters = types.ObjectValueMust(icmpParametersTypes, map[string]attr.Value{ - "code": types.Int64Value(1), - "type": types.Int64Value(2), -}) - -var fixtureIcmpParameters = iaas.ICMPParameters{ - Code: utils.Ptr(int64(1)), - Type: utils.Ptr(int64(2)), -} - -var fixtureModelPortRange = types.ObjectValueMust(portRangeTypes, map[string]attr.Value{ - "max": types.Int64Value(2), - "min": types.Int64Value(1), -}) - -var fixturePortRange = iaas.PortRange{ - Max: utils.Ptr(int64(2)), - Min: utils.Ptr(int64(1)), -} - -var fixtureModelProtocol = types.ObjectValueMust(protocolTypes, map[string]attr.Value{ - "name": types.StringValue("name"), - "number": types.Int64Value(1), -}) - -var fixtureProtocol = iaas.Protocol{ - Name: utils.Ptr("name"), - Number: utils.Ptr(int64(1)), -} - -var fixtureModelCreateProtocol = types.ObjectValueMust(protocolTypes, map[string]attr.Value{ - "name": types.StringValue("name"), - "number": types.Int64Null(), -}) - -var fixtureCreateProtocol = iaas.CreateProtocol{ - String: utils.Ptr("name"), -} - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.SecurityGroupRule - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - }, - input: &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,sgid,sgrid"), - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Direction: types.StringNull(), - Description: types.StringNull(), - EtherType: types.StringNull(), - IpRange: types.StringNull(), - RemoteSecurityGroupId: types.StringNull(), - IcmpParameters: types.ObjectNull(icmpParametersTypes), - PortRange: types.ObjectNull(portRangeTypes), - Protocol: types.ObjectNull(protocolTypes), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - Description: utils.Ptr("desc"), - Direction: utils.Ptr("ingress"), - Ethertype: utils.Ptr("ether"), - IpRange: utils.Ptr("iprange"), - RemoteSecurityGroupId: utils.Ptr("remote"), - IcmpParameters: &fixtureIcmpParameters, - PortRange: &fixturePortRange, - Protocol: &fixtureProtocol, - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,sgid,sgrid"), - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Direction: types.StringValue("ingress"), - Description: types.StringValue("desc"), - EtherType: types.StringValue("ether"), - IpRange: types.StringValue("iprange"), - RemoteSecurityGroupId: types.StringValue("remote"), - IcmpParameters: fixtureModelIcmpParameters, - PortRange: fixtureModelPortRange, - Protocol: fixtureModelProtocol, - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "protocol_only_with_name", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{ - "name": types.StringValue("name"), - "number": types.Int64Null(), - }), - }, - input: &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - Protocol: &fixtureProtocol, - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,sgid,sgrid"), - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Direction: types.StringNull(), - Description: types.StringNull(), - EtherType: types.StringNull(), - IpRange: types.StringNull(), - RemoteSecurityGroupId: types.StringNull(), - IcmpParameters: types.ObjectNull(icmpParametersTypes), - PortRange: types.ObjectNull(portRangeTypes), - Protocol: fixtureModelProtocol, - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "protocol_only_with_number", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{ - "name": types.StringNull(), - "number": types.Int64Value(1), - }), - }, - input: &iaas.SecurityGroupRule{ - Id: utils.Ptr("sgrid"), - Protocol: &fixtureProtocol, - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,sgid,sgrid"), - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - SecurityGroupRuleId: types.StringValue("sgrid"), - Direction: types.StringNull(), - Description: types.StringNull(), - EtherType: types.StringNull(), - IpRange: types.StringNull(), - RemoteSecurityGroupId: types.StringNull(), - IcmpParameters: types.ObjectNull(icmpParametersTypes), - PortRange: types.ObjectNull(portRangeTypes), - Protocol: fixtureModelProtocol, - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - SecurityGroupId: types.StringValue("sgid"), - }, - input: &iaas.SecurityGroupRule{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateSecurityGroupRulePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &iaas.CreateSecurityGroupRulePayload{}, - true, - }, - { - "default_ok", - &Model{ - Description: types.StringValue("desc"), - Direction: types.StringValue("ingress"), - IcmpParameters: fixtureModelIcmpParameters, - PortRange: fixtureModelPortRange, - Protocol: fixtureModelCreateProtocol, - }, - &iaas.CreateSecurityGroupRulePayload{ - Description: utils.Ptr("desc"), - Direction: utils.Ptr("ingress"), - IcmpParameters: &fixtureIcmpParameters, - PortRange: &fixturePortRange, - Protocol: &fixtureCreateProtocol, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var icmpParameters *icmpParametersModel - var portRange *portRangeModel - var protocol *protocolModel - if tt.input != nil { - if !(tt.input.IcmpParameters.IsNull() || tt.input.IcmpParameters.IsUnknown()) { - icmpParameters = &icmpParametersModel{} - diags := tt.input.IcmpParameters.As(context.Background(), icmpParameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting icmp parameters: %v", diags.Errors()) - } - } - - if !(tt.input.PortRange.IsNull() || tt.input.PortRange.IsUnknown()) { - portRange = &portRangeModel{} - diags := tt.input.PortRange.As(context.Background(), portRange, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting port range: %v", diags.Errors()) - } - } - - if !(tt.input.Protocol.IsNull() || tt.input.Protocol.IsUnknown()) { - protocol = &protocolModel{} - diags := tt.input.Protocol.As(context.Background(), protocol, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting protocol: %v", diags.Errors()) - } - } - } - - output, err := toCreatePayload(tt.input, icmpParameters, portRange, protocol) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/server/const.go b/stackit/internal/services/iaas/server/const.go deleted file mode 100644 index 4da17e6a..00000000 --- a/stackit/internal/services/iaas/server/const.go +++ /dev/null @@ -1,176 +0,0 @@ -package server - -const markdownDescription = ` -Server resource schema. Must have a region specified in the provider configuration.` + "\n" + ` -## Example Usage` + "\n" + ` - -### With key pair` + "\n" + - "```terraform" + ` -resource "stackit_key_pair" "keypair" { - name = "example-key-pair" - public_key = chomp(file("path/to/id_rsa.pub")) -} - -resource "stackit_server" "user-data-from-file" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - name = "example-server" - machine_type = "g2i.1" - keypair_name = stackit_key_pair.keypair.name - user_data = file("${path.module}/cloud-init.yaml") -} -` + "\n```" + ` - -### Boot from volume` + "\n" + - "```terraform" + ` -resource "stackit_server" "boot-from-volume" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-server" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - availability_zone = "eu01-1" - machine_type = "g2i.1" - keypair_name = "example-keypair" -} -` + "\n```" + ` - -### Boot from existing volume` + "\n" + - "```terraform" + ` -resource "stackit_volume" "example-volume" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - size = 12 - source = { - type = "image" - id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - name = "example-volume" - availability_zone = "eu01-1" -} - -resource "stackit_server" "boot-from-volume" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-server" - boot_volume = { - source_type = "volume" - source_id = stackit_volume.example-volume.volume_id - } - availability_zone = "eu01-1" - machine_type = "g2i.1" - keypair_name = stackit_key_pair.keypair.name -} -` + "\n```" + ` - -### Network setup` + "\n" + - "```terraform" + ` -resource "stackit_network" "network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-network" - nameservers = ["192.0.2.0", "198.51.100.0", "203.0.113.0"] - ipv4_prefix_length = 24 -} - -resource "stackit_security_group" "sec-group" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-security-group" - stateful = true -} - -resource "stackit_security_group_rule" "rule" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - security_group_id = stackit_security_group.sec-group.security_group_id - direction = "ingress" - ether_type = "IPv4" -} - -resource "stackit_network_interface" "nic" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - network_id = stackit_network.network.network_id - security_group_ids = [stackit_security_group.sec-group.security_group_id] -} - -resource "stackit_server" "server-with-network" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-server" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - machine_type = "g2i.1" - keypair_name = stackit_key_pair.keypair.name - network_interfaces = [ - stackit_network_interface.nic.network_interface_id - ] -} - -resource "stackit_public_ip" "public-ip" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - network_interface_id = stackit_network_interface.nic.network_interface_id -} -` + "\n```" + ` - -### Server with attached volume` + "\n" + - "```terraform" + ` -resource "stackit_volume" "example-volume" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - size = 12 - performance_class = "storage_premium_perf6" - name = "example-volume" - availability_zone = "eu01-1" -} - -resource "stackit_server" "server-with-volume" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "example-server" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - availability_zone = "eu01-1" - machine_type = "g2i.1" - keypair_name = stackit_key_pair.keypair.name -} - -resource "stackit_server_volume_attach" "attach_volume" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - server_id = stackit_server.server-with-volume.server_id - volume_id = stackit_volume.example-volume.volume_id -} -` + "\n```" + ` - -### Server with user data (cloud-init)` + "\n" + - "```terraform" + ` -resource "stackit_server" "user-data" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - name = "example-server" - machine_type = "g2i.1" - keypair_name = stackit_key_pair.keypair.name - user_data = "#!/bin/bash\n/bin/su" -} - -resource "stackit_server" "user-data-from-file" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - boot_volume = { - size = 64 - source_type = "image" - source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - } - name = "example-server" - machine_type = "g2i.1" - keypair_name = stackit_key_pair.keypair.name - user_data = file("${path.module}/cloud-init.yaml") -} -` + "\n```" diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go deleted file mode 100644 index f68f5563..00000000 --- a/stackit/internal/services/iaas/server/datasource.go +++ /dev/null @@ -1,325 +0,0 @@ -package server - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &serverDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ServerId types.String `tfsdk:"server_id"` - MachineType types.String `tfsdk:"machine_type"` - Name types.String `tfsdk:"name"` - AvailabilityZone types.String `tfsdk:"availability_zone"` - BootVolume types.Object `tfsdk:"boot_volume"` - ImageId types.String `tfsdk:"image_id"` - NetworkInterfaces types.List `tfsdk:"network_interfaces"` - KeypairName types.String `tfsdk:"keypair_name"` - Labels types.Map `tfsdk:"labels"` - AffinityGroup types.String `tfsdk:"affinity_group"` - UserData types.String `tfsdk:"user_data"` - CreatedAt types.String `tfsdk:"created_at"` - LaunchedAt types.String `tfsdk:"launched_at"` - UpdatedAt types.String `tfsdk:"updated_at"` -} - -var bootVolumeDataTypes = map[string]attr.Type{ - "id": basetypes.StringType{}, - "delete_on_termination": basetypes.BoolType{}, -} - -// NewServerDataSource is a helper function to simplify the provider implementation. -func NewServerDataSource() datasource.DataSource { - return &serverDataSource{} -} - -// serverDataSource is the data source implementation. -type serverDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *serverDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server" -} - -func (d *serverDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the datasource. -func (d *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Server datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the server is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "server_id": schema.StringAttribute{ - Description: "The server ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the server.", - Computed: true, - }, - "machine_type": schema.StringAttribute{ - MarkdownDescription: "Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/)", - Computed: true, - }, - "availability_zone": schema.StringAttribute{ - Description: "The availability zone of the server.", - Computed: true, - }, - "boot_volume": schema.SingleNestedAttribute{ - Description: "The boot volume for the server", - Computed: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "The ID of the boot volume", - Computed: true, - }, - "delete_on_termination": schema.BoolAttribute{ - Description: "Delete the volume during the termination of the server.", - Computed: true, - }, - }, - }, - "image_id": schema.StringAttribute{ - Description: "The image ID to be used for an ephemeral disk on the server.", - Computed: true, - }, - "network_interfaces": schema.ListAttribute{ - Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server.", - Computed: true, - ElementType: types.StringType, - }, - "keypair_name": schema.StringAttribute{ - Description: "The name of the keypair used during server creation.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - "affinity_group": schema.StringAttribute{ - Description: "The affinity group the server is assigned to.", - Computed: true, - }, - "user_data": schema.StringAttribute{ - Description: "User data that is passed via cloud-init to the server.", - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: "Date-time when the server was created", - Computed: true, - }, - "launched_at": schema.StringAttribute{ - Description: "Date-time when the server was launched", - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: "Date-time when the server was updated", - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - - serverReq := d.client.GetServer(ctx, projectId, region, serverId) - serverReq = serverReq.Details(true) - serverResp, err := serverReq.Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading server", - fmt.Sprintf("Server with ID %q does not exist in project %q.", serverId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapDataSourceFields(ctx, serverResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "server read") -} - -func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *DataSourceModel, region string) error { - if serverResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var serverId string - if model.ServerId.ValueString() != "" { - serverId = model.ServerId.ValueString() - } else if serverResp.Id != nil { - serverId = *serverResp.Id - } else { - return fmt.Errorf("server id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, serverId) - model.Region = types.StringValue(region) - - labels, err := iaasUtils.MapLabels(ctx, serverResp.Labels, model.Labels) - if err != nil { - return err - } - - var createdAt basetypes.StringValue - if serverResp.CreatedAt != nil { - createdAtValue := *serverResp.CreatedAt - createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - var updatedAt basetypes.StringValue - if serverResp.UpdatedAt != nil { - updatedAtValue := *serverResp.UpdatedAt - updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) - } - var launchedAt basetypes.StringValue - if serverResp.LaunchedAt != nil { - launchedAtValue := *serverResp.LaunchedAt - launchedAt = types.StringValue(launchedAtValue.Format(time.RFC3339)) - } - if serverResp.Nics != nil { - var respNics []string - for _, nic := range *serverResp.Nics { - respNics = append(respNics, *nic.NicId) - } - nicTF, diags := types.ListValueFrom(ctx, types.StringType, respNics) - if diags.HasError() { - return fmt.Errorf("failed to map networkInterfaces: %w", core.DiagsToError(diags)) - } - - model.NetworkInterfaces = nicTF - } else { - model.NetworkInterfaces = types.ListNull(types.StringType) - } - - if serverResp.BootVolume != nil { - bootVolume, diags := types.ObjectValue(bootVolumeDataTypes, map[string]attr.Value{ - "id": types.StringPointerValue(serverResp.BootVolume.Id), - "delete_on_termination": types.BoolPointerValue(serverResp.BootVolume.DeleteOnTermination), - }) - if diags.HasError() { - return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags)) - } - model.BootVolume = bootVolume - } else { - model.BootVolume = types.ObjectNull(bootVolumeDataTypes) - } - - if serverResp.UserData != nil && len(*serverResp.UserData) > 0 { - model.UserData = types.StringValue(string(*serverResp.UserData)) - } - - model.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone) - model.ServerId = types.StringValue(serverId) - model.MachineType = types.StringPointerValue(serverResp.MachineType) - - model.Name = types.StringPointerValue(serverResp.Name) - model.Labels = labels - model.ImageId = types.StringPointerValue(serverResp.ImageId) - model.KeypairName = types.StringPointerValue(serverResp.KeypairName) - model.AffinityGroup = types.StringPointerValue(serverResp.AffinityGroup) - model.CreatedAt = createdAt - model.UpdatedAt = updatedAt - model.LaunchedAt = launchedAt - - return nil -} diff --git a/stackit/internal/services/iaas/server/datasource_test.go b/stackit/internal/services/iaas/server/datasource_test.go deleted file mode 100644 index 56c2be53..00000000 --- a/stackit/internal/services/iaas/server/datasource_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package server - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapDataSourceFields(t *testing.T) { - type args struct { - state DataSourceModel - input *iaas.Server - region string - } - tests := []struct { - description string - args args - expected DataSourceModel - isValid bool - }{ - { - description: "default_values", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - }, - input: &iaas.Server{ - Id: utils.Ptr("sid"), - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu01,sid"), - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Name: types.StringNull(), - AvailabilityZone: types.StringNull(), - Labels: types.MapNull(types.StringType), - ImageId: types.StringNull(), - NetworkInterfaces: types.ListNull(types.StringType), - KeypairName: types.StringNull(), - AffinityGroup: types.StringNull(), - UserData: types.StringNull(), - CreatedAt: types.StringNull(), - UpdatedAt: types.StringNull(), - LaunchedAt: types.StringNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.Server{ - Id: utils.Ptr("sid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", - }, - ImageId: utils.Ptr("image_id"), - Nics: &[]iaas.ServerNetwork{ - { - NicId: utils.Ptr("nic1"), - }, - { - NicId: utils.Ptr("nic2"), - }, - }, - KeypairName: utils.Ptr("keypair_name"), - AffinityGroup: utils.Ptr("group_id"), - CreatedAt: utils.Ptr(testTimestamp()), - UpdatedAt: utils.Ptr(testTimestamp()), - LaunchedAt: utils.Ptr(testTimestamp()), - Status: utils.Ptr("active"), - }, - region: "eu02", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu02,sid"), - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Name: types.StringValue("name"), - AvailabilityZone: types.StringValue("zone"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - ImageId: types.StringValue("image_id"), - NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nic1"), - types.StringValue("nic2"), - }), - KeypairName: types.StringValue("keypair_name"), - AffinityGroup: types.StringValue("group_id"), - CreatedAt: types.StringValue(testTimestampValue), - UpdatedAt: types.StringValue(testTimestampValue), - LaunchedAt: types.StringValue(testTimestampValue), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - input: &iaas.Server{ - Id: utils.Ptr("sid"), - }, - region: "eu01", - }, - expected: DataSourceModel{ - Id: types.StringValue("pid,eu01,sid"), - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Name: types.StringNull(), - AvailabilityZone: types.StringNull(), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - ImageId: types.StringNull(), - NetworkInterfaces: types.ListNull(types.StringType), - KeypairName: types.StringNull(), - AffinityGroup: types.StringNull(), - UserData: types.StringNull(), - CreatedAt: types.StringNull(), - UpdatedAt: types.StringNull(), - LaunchedAt: types.StringNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: DataSourceModel{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.Server{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/server/resource.go b/stackit/internal/services/iaas/server/resource.go deleted file mode 100644 index 5af72de8..00000000 --- a/stackit/internal/services/iaas/server/resource.go +++ /dev/null @@ -1,1108 +0,0 @@ -package server - -import ( - "context" - "encoding/base64" - "fmt" - "net/http" - "regexp" - "strings" - "time" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "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/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &serverResource{} - _ resource.ResourceWithConfigure = &serverResource{} - _ resource.ResourceWithImportState = &serverResource{} - _ resource.ResourceWithModifyPlan = &serverResource{} - - supportedSourceTypes = []string{"volume", "image"} - desiredStatusOptions = []string{modelStateActive, modelStateInactive, modelStateDeallocated} -) - -const ( - modelStateActive = "active" - modelStateInactive = "inactive" - modelStateDeallocated = "deallocated" -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ServerId types.String `tfsdk:"server_id"` - MachineType types.String `tfsdk:"machine_type"` - Name types.String `tfsdk:"name"` - AvailabilityZone types.String `tfsdk:"availability_zone"` - BootVolume types.Object `tfsdk:"boot_volume"` - ImageId types.String `tfsdk:"image_id"` - NetworkInterfaces types.List `tfsdk:"network_interfaces"` - KeypairName types.String `tfsdk:"keypair_name"` - Labels types.Map `tfsdk:"labels"` - AffinityGroup types.String `tfsdk:"affinity_group"` - UserData types.String `tfsdk:"user_data"` - CreatedAt types.String `tfsdk:"created_at"` - LaunchedAt types.String `tfsdk:"launched_at"` - UpdatedAt types.String `tfsdk:"updated_at"` - DesiredStatus types.String `tfsdk:"desired_status"` -} - -// Struct corresponding to Model.BootVolume -type bootVolumeModel struct { - Id types.String `tfsdk:"id"` - PerformanceClass types.String `tfsdk:"performance_class"` - Size types.Int64 `tfsdk:"size"` - SourceType types.String `tfsdk:"source_type"` - SourceId types.String `tfsdk:"source_id"` - DeleteOnTermination types.Bool `tfsdk:"delete_on_termination"` -} - -// Types corresponding to bootVolumeModel -var bootVolumeTypes = map[string]attr.Type{ - "performance_class": basetypes.StringType{}, - "size": basetypes.Int64Type{}, - "source_type": basetypes.StringType{}, - "source_id": basetypes.StringType{}, - "delete_on_termination": basetypes.BoolType{}, - "id": basetypes.StringType{}, -} - -// NewServerResource is a helper function to simplify the provider implementation. -func NewServerResource() resource.Resource { - return &serverResource{} -} - -// serverResource is the resource implementation. -type serverResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *serverResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *serverResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (r *serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var model Model - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - // convert boot volume model - var bootVolume = &bootVolumeModel{} - if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) { - diags := model.BootVolume.As(ctx, bootVolume, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return - } - } - - if !bootVolume.DeleteOnTermination.IsUnknown() && !bootVolume.DeleteOnTermination.IsNull() && !bootVolume.SourceType.IsUnknown() && !bootVolume.SourceType.IsNull() { - if bootVolume.SourceType != types.StringValue("image") { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring server", "You can only provide `delete_on_termination` for `source_type` `image`.") - } - } - - if model.NetworkInterfaces.IsNull() || model.NetworkInterfaces.IsUnknown() || len(model.NetworkInterfaces.Elements()) < 1 { - core.LogAndAddWarning(ctx, &resp.Diagnostics, "No network interfaces configured", "You have no network interfaces configured for this server. This will be a problem when you want to (re-)create this server. Please note that modifying the network interfaces for an existing server will result in a replacement of the resource. We will provide a clear migration path soon.") - } -} - -// ConfigValidators validates the resource configuration -func (r *serverResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { - return []resource.ConfigValidator{ - resourcevalidator.AtLeastOneOf( - path.MatchRoot("image_id"), - path.MatchRoot("boot_volume"), - ), - resourcevalidator.Conflicting( - path.MatchRoot("image_id"), - path.MatchRoot("boot_volume"), - ), - } -} - -// Configure adds the provider configured client to the resource. -func (r *serverResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - MarkdownDescription: markdownDescription, - Description: "Server resource schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the server is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "The server ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the server.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "machine_type": schema.StringAttribute{ - MarkdownDescription: "Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/)", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "availability_zone": schema.StringAttribute{ - Description: "The availability zone of the server.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Optional: true, - Computed: true, - }, - "boot_volume": schema.SingleNestedAttribute{ - Description: "The boot volume for the server", - Optional: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.RequiresReplace(), - }, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "The ID of the boot volume", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "performance_class": schema.StringAttribute{ - Description: "The performance class of the server.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "size": schema.Int64Attribute{ - Description: "The size of the boot volume in GB. Must be provided when `source_type` is `image`.", - Optional: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - }, - "source_type": schema.StringAttribute{ - Description: "The type of the source. " + utils.FormatPossibleValues(supportedSourceTypes...), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "source_id": schema.StringAttribute{ - Description: "The ID of the source, either image ID or volume ID", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "delete_on_termination": schema.BoolAttribute{ - Description: "Delete the volume during the termination of the server. Only allowed when `source_type` is `image`.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - }, - }, - }, - "image_id": schema.StringAttribute{ - Description: "The image ID to be used for an ephemeral disk on the server.", - Optional: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "network_interfaces": schema.ListAttribute{ - Description: "The IDs of network interfaces which should be attached to the server. Updating it will recreate the server. **Required when (re-)creating servers. Still marked as optional in the schema to not introduce breaking changes. There will be a migration path for this field soon.**", - Optional: true, - ElementType: types.StringType, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - validate.UUID(), - validate.NoSeparator(), - ), - }, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - }, - "keypair_name": schema.StringAttribute{ - Description: "The name of the keypair used during server creation.", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - "affinity_group": schema.StringAttribute{ - Description: "The affinity group the server is assigned to.", - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(36), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`), - "must match expression"), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "user_data": schema.StringAttribute{ - Description: "User data that is passed via cloud-init to the server.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "created_at": schema.StringAttribute{ - Description: "Date-time when the server was created", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "launched_at": schema.StringAttribute{ - Description: "Date-time when the server was launched", - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: "Date-time when the server was updated", - Computed: true, - }, - "desired_status": schema.StringAttribute{ - Description: "The desired status of the server resource. " + utils.FormatPossibleValues(desiredStatusOptions...), - Optional: true, - Validators: []validator.String{ - stringvalidator.OneOf(desiredStatusOptions...), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - desiredStateModifier{}, - }, - }, - }, - } -} - -var _ planmodifier.String = desiredStateModifier{} - -type desiredStateModifier struct { -} - -// Description implements planmodifier.String. -func (d desiredStateModifier) Description(context.Context) string { - return "validates desired state transition" -} - -// MarkdownDescription implements planmodifier.String. -func (d desiredStateModifier) MarkdownDescription(ctx context.Context) string { - return d.Description(ctx) -} - -// PlanModifyString implements planmodifier.String. -func (d desiredStateModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { //nolint: gocritic //signature is defined by terraform api - // Retrieve values from plan - var ( - planState types.String - currentState types.String - ) - resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("desired_status"), &planState)...) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("desired_status"), ¤tState)...) - if resp.Diagnostics.HasError() { - return - } - - if currentState.ValueString() == modelStateDeallocated && planState.ValueString() == modelStateInactive { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error changing server state", "Server state change from deallocated to inactive is not possible") - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - ctx = core.InitProviderContext(ctx) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new server - - server, err := r.client.CreateServer(ctx, projectId, region).CreateServerPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - serverId := *server.Id - _, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err)) - return - } - ctx = tflog.SetField(ctx, "server_id", serverId) - - // Get Server with details - serverReq := r.client.GetServer(ctx, projectId, region, serverId) - serverReq = serverReq.Details(true) - server, err = serverReq.Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("get server details: %v", err)) - } - - // Map response body to schema - err = mapFields(ctx, server, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("update server state: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server created") -} - -// serverControlClient provides a mockable interface for the necessary -// client operations in [updateServerStatus] -type serverControlClient interface { - wait.APIClientInterface - StartServerExecute(ctx context.Context, projectId string, region string, serverId string) error - StopServerExecute(ctx context.Context, projectId string, region string, serverId string) error - DeallocateServerExecute(ctx context.Context, projectId string, region string, serverId string) error -} - -func startServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error { - tflog.Debug(ctx, "starting server to enter active state") - if err := client.StartServerExecute(ctx, projectId, region, serverId); err != nil { - return fmt.Errorf("cannot start server: %w", err) - } - _, err := wait.StartServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("cannot check started server: %w", err) - } - return nil -} - -func stopServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error { - tflog.Debug(ctx, "stopping server to enter inactive state") - if err := client.StopServerExecute(ctx, projectId, region, serverId); err != nil { - return fmt.Errorf("cannot stop server: %w", err) - } - _, err := wait.StopServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("cannot check stopped server: %w", err) - } - return nil -} - -func deallocateServer(ctx context.Context, client serverControlClient, projectId, region, serverId string) error { - tflog.Debug(ctx, "deallocating server to enter shelved state") - if err := client.DeallocateServerExecute(ctx, projectId, region, serverId); err != nil { - return fmt.Errorf("cannot deallocate server: %w", err) - } - _, err := wait.DeallocateServerWaitHandler(ctx, client, projectId, region, serverId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("cannot check deallocated server: %w", err) - } - return nil -} - -// updateServerStatus applies the appropriate server state changes for the actual current and the intended state -func updateServerStatus(ctx context.Context, client serverControlClient, currentState *string, model *Model, region string) error { - if currentState == nil { - tflog.Warn(ctx, "no current state available, not updating server state") - return nil - } - switch *currentState { - case wait.ServerActiveStatus: - switch strings.ToUpper(model.DesiredStatus.ValueString()) { - case wait.ServerInactiveStatus: - if err := stopServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - - case wait.ServerDeallocatedStatus: - - if err := deallocateServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - default: - tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) - if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - } - case wait.ServerInactiveStatus: - switch strings.ToUpper(model.DesiredStatus.ValueString()) { - case wait.ServerActiveStatus: - if err := startServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - case wait.ServerDeallocatedStatus: - if err := deallocateServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - - default: - tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) - if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - } - case wait.ServerDeallocatedStatus: - switch strings.ToUpper(model.DesiredStatus.ValueString()) { - case wait.ServerActiveStatus: - if err := startServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - - case wait.ServerInactiveStatus: - if err := stopServer(ctx, client, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - default: - tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) - if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), region, model.ServerId.ValueString()); err != nil { - return err - } - } - default: - tflog.Debug(ctx, "not updating server state") - } - - return nil -} - -// Read refreshes the Terraform state with the latest data. -func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - - serverReq := r.client.GetServer(ctx, projectId, region, serverId) - serverReq = serverReq.Details(true) - serverResp, err := serverReq.Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, serverResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "server read") -} - -func (r *serverResource) updateServerAttributes(ctx context.Context, model, stateModel *Model, region string) (*iaas.Server, error) { - // Generate API request body from model - payload, err := toUpdatePayload(ctx, model, stateModel.Labels) - if err != nil { - return nil, fmt.Errorf("Creating API payload: %w", err) - } - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - - var updatedServer *iaas.Server - // Update existing server - updatedServer, err = r.client.UpdateServer(ctx, projectId, region, serverId).UpdateServerPayload(*payload).Execute() - if err != nil { - return nil, fmt.Errorf("Calling API: %w", err) - } - - // Update machine type - modelMachineType := conversion.StringValueToPointer(model.MachineType) - if modelMachineType != nil && updatedServer.MachineType != nil && *modelMachineType != *updatedServer.MachineType { - payload := iaas.ResizeServerPayload{ - MachineType: modelMachineType, - } - err := r.client.ResizeServer(ctx, projectId, region, serverId).ResizeServerPayload(payload).Execute() - if err != nil { - return nil, fmt.Errorf("Resizing the server, calling API: %w", err) - } - - _, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx) - if err != nil { - return nil, fmt.Errorf("server resize waiting: %w", err) - } - // Update server model because the API doesn't return a server object as response - updatedServer.MachineType = modelMachineType - } - return updatedServer, nil -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var ( - server *iaas.Server - err error - ) - if server, err = r.client.GetServer(ctx, projectId, region, serverId).Execute(); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error retrieving server state", fmt.Sprintf("Getting server state: %v", err)) - } - - if model.DesiredStatus.ValueString() == modelStateDeallocated { - // if the target state is "deallocated", we have to perform the server update first - // and then shelve it afterwards. A shelved server cannot be updated - _, err = r.updateServerAttributes(ctx, &model, &stateModel, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) - return - } - } else { - // potentially unfreeze first and update afterwards - if err := updateServerStatus(ctx, r.client, server.Status, &model, region); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) - return - } - - _, err = r.updateServerAttributes(ctx, &model, &stateModel, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - } - - // Re-fetch the server data, to get the details values. - serverReq := r.client.GetServer(ctx, projectId, region, serverId) - serverReq = serverReq.Details(true) - updatedServer, err := serverReq.Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Calling API: %v", err)) - return - } - - err = mapFields(ctx, updatedServer, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "server updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - - // Delete existing server - err := r.client.DeleteServer(ctx, projectId, region, serverId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteServerWaitHandler(ctx, r.client, projectId, region, serverId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("server deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "server deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id -func (r *serverResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing server", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "server_id": idParts[2], - }) - - tflog.Info(ctx, "server state imported") -} - -func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model, region string) error { - if serverResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var serverId string - if model.ServerId.ValueString() != "" { - serverId = model.ServerId.ValueString() - } else if serverResp.Id != nil { - serverId = *serverResp.Id - } else { - return fmt.Errorf("server id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, serverId) - model.Region = types.StringValue(region) - - labels, err := iaasUtils.MapLabels(ctx, serverResp.Labels, model.Labels) - if err != nil { - return err - } - - var createdAt basetypes.StringValue - if serverResp.CreatedAt != nil { - createdAtValue := *serverResp.CreatedAt - createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - var updatedAt basetypes.StringValue - if serverResp.UpdatedAt != nil { - updatedAtValue := *serverResp.UpdatedAt - updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) - } - var launchedAt basetypes.StringValue - if serverResp.LaunchedAt != nil { - launchedAtValue := *serverResp.LaunchedAt - launchedAt = types.StringValue(launchedAtValue.Format(time.RFC3339)) - } - if serverResp.Nics != nil { - var respNics []string - for _, nic := range *serverResp.Nics { - respNics = append(respNics, *nic.NicId) - } - - var modelNics []string - for _, modelNic := range model.NetworkInterfaces.Elements() { - modelNicString, ok := modelNic.(types.String) - if !ok { - return fmt.Errorf("type assertion for network interfaces failed") - } - modelNics = append(modelNics, modelNicString.ValueString()) - } - - var filteredNics []string - for _, modelNic := range modelNics { - for _, nic := range respNics { - if nic == modelNic { - filteredNics = append(filteredNics, nic) - break - } - } - } - - // Sorts the filteredNics based on the modelNics order - resultNics := utils.ReconcileStringSlices(modelNics, filteredNics) - - if len(resultNics) != 0 { - nicTF, diags := types.ListValueFrom(ctx, types.StringType, resultNics) - if diags.HasError() { - return fmt.Errorf("failed to map networkInterfaces: %w", core.DiagsToError(diags)) - } - - model.NetworkInterfaces = nicTF - } else { - model.NetworkInterfaces = types.ListNull(types.StringType) - } - } else { - model.NetworkInterfaces = types.ListNull(types.StringType) - } - - if serverResp.BootVolume != nil { - // convert boot volume model - var bootVolumeModel = &bootVolumeModel{} - if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) { - diags := model.BootVolume.As(ctx, bootVolumeModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags)) - } - } - - // Only the id and delete_on_termination is returned via response. - // Take the other values from the model. - bootVolume, diags := types.ObjectValue(bootVolumeTypes, map[string]attr.Value{ - "id": types.StringPointerValue(serverResp.BootVolume.Id), - "delete_on_termination": types.BoolPointerValue(serverResp.BootVolume.DeleteOnTermination), - "source_id": bootVolumeModel.SourceId, - "size": bootVolumeModel.Size, - "source_type": bootVolumeModel.SourceType, - "performance_class": bootVolumeModel.PerformanceClass, - }) - if diags.HasError() { - return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags)) - } - model.BootVolume = bootVolume - } else { - model.BootVolume = types.ObjectNull(bootVolumeTypes) - } - - model.ServerId = types.StringValue(serverId) - model.MachineType = types.StringPointerValue(serverResp.MachineType) - - // Proposed fix: If the server is deallocated, it has no availability zone anymore - // reactivation will then _change_ the availability zone again, causing terraform - // to destroy and recreate the resource, which is not intended. So we skip the zone - // when the server is deallocated to retain the original zone until the server - // is activated again - if serverResp.Status != nil && *serverResp.Status != wait.ServerDeallocatedStatus { - model.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone) - } - - if serverResp.UserData != nil && len(*serverResp.UserData) > 0 { - model.UserData = types.StringValue(string(*serverResp.UserData)) - } - model.Name = types.StringPointerValue(serverResp.Name) - model.Labels = labels - model.ImageId = types.StringPointerValue(serverResp.ImageId) - model.KeypairName = types.StringPointerValue(serverResp.KeypairName) - model.AffinityGroup = types.StringPointerValue(serverResp.AffinityGroup) - model.CreatedAt = createdAt - model.UpdatedAt = updatedAt - model.LaunchedAt = launchedAt - - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - var bootVolume = &bootVolumeModel{} - if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) { - diags := model.BootVolume.As(ctx, bootVolume, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags)) - } - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - var bootVolumePayload *iaas.ServerBootVolume - if !bootVolume.SourceId.IsNull() && !bootVolume.SourceType.IsNull() { - bootVolumePayload = &iaas.ServerBootVolume{ - PerformanceClass: conversion.StringValueToPointer(bootVolume.PerformanceClass), - Size: conversion.Int64ValueToPointer(bootVolume.Size), - Source: &iaas.BootVolumeSource{ - Id: conversion.StringValueToPointer(bootVolume.SourceId), - Type: conversion.StringValueToPointer(bootVolume.SourceType), - }, - } - if !bootVolume.DeleteOnTermination.IsNull() && !bootVolume.DeleteOnTermination.IsUnknown() && bootVolume.DeleteOnTermination.ValueBool() { - // it is set and true, adjust payload - bootVolumePayload.DeleteOnTermination = conversion.BoolValueToPointer(bootVolume.DeleteOnTermination) - } - } - - var userData *[]byte - if !model.UserData.IsNull() && !model.UserData.IsUnknown() { - src := []byte(model.UserData.ValueString()) - encodedUserData := make([]byte, base64.StdEncoding.EncodedLen(len(src))) - base64.StdEncoding.Encode(encodedUserData, src) - userData = &encodedUserData - } - - if model.NetworkInterfaces.IsNull() || model.NetworkInterfaces.IsUnknown() { - return nil, fmt.Errorf("nil network interfaces") - } - var nicIds []string - for _, nic := range model.NetworkInterfaces.Elements() { - nicString, ok := nic.(types.String) - if !ok { - return nil, fmt.Errorf("type assertion failed") - } - nicIds = append(nicIds, nicString.ValueString()) - } - - network := &iaas.CreateServerPayloadAllOfNetworking{ - CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ - NicIds: &nicIds, - }, - } - - return &iaas.CreateServerPayload{ - AffinityGroup: conversion.StringValueToPointer(model.AffinityGroup), - AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone), - BootVolume: bootVolumePayload, - ImageId: conversion.StringValueToPointer(model.ImageId), - KeypairName: conversion.StringValueToPointer(model.KeypairName), - Labels: &labels, - Name: conversion.StringValueToPointer(model.Name), - Networking: network, - MachineType: conversion.StringValueToPointer(model.MachineType), - UserData: userData, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateServerPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.UpdateServerPayload{ - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - }, nil -} diff --git a/stackit/internal/services/iaas/server/resource_test.go b/stackit/internal/services/iaas/server/resource_test.go deleted file mode 100644 index ad1c7074..00000000 --- a/stackit/internal/services/iaas/server/resource_test.go +++ /dev/null @@ -1,623 +0,0 @@ -package server - -import ( - "context" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" -) - -const ( - userData = "user_data" - base64EncodedUserData = "dXNlcl9kYXRh" - testTimestampValue = "2006-01-02T15:04:05Z" -) - -func testTimestamp() time.Time { - timestamp, _ := time.Parse(time.RFC3339, testTimestampValue) - return timestamp -} - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.Server - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - }, - input: &iaas.Server{ - Id: utils.Ptr("sid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,sid"), - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Name: types.StringNull(), - AvailabilityZone: types.StringNull(), - Labels: types.MapNull(types.StringType), - ImageId: types.StringNull(), - NetworkInterfaces: types.ListNull(types.StringType), - KeypairName: types.StringNull(), - AffinityGroup: types.StringNull(), - UserData: types.StringNull(), - CreatedAt: types.StringNull(), - UpdatedAt: types.StringNull(), - LaunchedAt: types.StringNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.Server{ - Id: utils.Ptr("sid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", - }, - ImageId: utils.Ptr("image_id"), - Nics: &[]iaas.ServerNetwork{ - { - NicId: utils.Ptr("nic1"), - }, - { - NicId: utils.Ptr("nic2"), - }, - }, - KeypairName: utils.Ptr("keypair_name"), - AffinityGroup: utils.Ptr("group_id"), - CreatedAt: utils.Ptr(testTimestamp()), - UpdatedAt: utils.Ptr(testTimestamp()), - LaunchedAt: utils.Ptr(testTimestamp()), - Status: utils.Ptr("active"), - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,sid"), - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Name: types.StringValue("name"), - AvailabilityZone: types.StringValue("zone"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - ImageId: types.StringValue("image_id"), - NetworkInterfaces: types.ListNull(types.StringType), - KeypairName: types.StringValue("keypair_name"), - AffinityGroup: types.StringValue("group_id"), - CreatedAt: types.StringValue(testTimestampValue), - UpdatedAt: types.StringValue(testTimestampValue), - LaunchedAt: types.StringValue(testTimestampValue), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - input: &iaas.Server{ - Id: utils.Ptr("sid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,sid"), - ProjectId: types.StringValue("pid"), - ServerId: types.StringValue("sid"), - Name: types.StringNull(), - AvailabilityZone: types.StringNull(), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - ImageId: types.StringNull(), - NetworkInterfaces: types.ListNull(types.StringType), - KeypairName: types.StringNull(), - AffinityGroup: types.StringNull(), - UserData: types.StringNull(), - CreatedAt: types.StringNull(), - UpdatedAt: types.StringNull(), - LaunchedAt: types.StringNull(), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.Server{}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.CreateServerPayload - isValid bool - }{ - { - description: "ok", - input: &Model{ - Name: types.StringValue("name"), - AvailabilityZone: types.StringValue("zone"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - BootVolume: types.ObjectValueMust(bootVolumeTypes, map[string]attr.Value{ - "performance_class": types.StringValue("class"), - "size": types.Int64Value(1), - "source_type": types.StringValue("type"), - "source_id": types.StringValue("id"), - "delete_on_termination": types.BoolUnknown(), - "id": types.StringValue("id"), - }), - ImageId: types.StringValue("image"), - KeypairName: types.StringValue("keypair"), - MachineType: types.StringValue("machine_type"), - UserData: types.StringValue(userData), - NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nic1"), - types.StringValue("nic2"), - }), - }, - expected: &iaas.CreateServerPayload{ - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", - }, - BootVolume: &iaas.ServerBootVolume{ - PerformanceClass: utils.Ptr("class"), - Size: utils.Ptr(int64(1)), - Source: &iaas.BootVolumeSource{ - Type: utils.Ptr("type"), - Id: utils.Ptr("id"), - }, - }, - ImageId: utils.Ptr("image"), - KeypairName: utils.Ptr("keypair"), - MachineType: utils.Ptr("machine_type"), - UserData: utils.Ptr([]byte(base64EncodedUserData)), - Networking: &iaas.CreateServerPayloadAllOfNetworking{ - CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ - NicIds: &[]string{"nic1", "nic2"}, - }, - }, - }, - isValid: true, - }, - { - description: "delete on termination is set to true", - input: &Model{ - Name: types.StringValue("name"), - AvailabilityZone: types.StringValue("zone"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - BootVolume: types.ObjectValueMust(bootVolumeTypes, map[string]attr.Value{ - "performance_class": types.StringValue("class"), - "size": types.Int64Value(1), - "source_type": types.StringValue("image"), - "source_id": types.StringValue("id"), - "delete_on_termination": types.BoolValue(true), - "id": types.StringValue("id"), - }), - ImageId: types.StringValue("image"), - KeypairName: types.StringValue("keypair"), - MachineType: types.StringValue("machine_type"), - UserData: types.StringValue(userData), - NetworkInterfaces: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("nic1"), - types.StringValue("nic2"), - }), - }, - expected: &iaas.CreateServerPayload{ - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", - }, - BootVolume: &iaas.ServerBootVolume{ - PerformanceClass: utils.Ptr("class"), - Size: utils.Ptr(int64(1)), - Source: &iaas.BootVolumeSource{ - Type: utils.Ptr("image"), - Id: utils.Ptr("id"), - }, - DeleteOnTermination: utils.Ptr(true), - }, - ImageId: utils.Ptr("image"), - KeypairName: utils.Ptr("keypair"), - MachineType: utils.Ptr("machine_type"), - UserData: utils.Ptr([]byte(base64EncodedUserData)), - Networking: &iaas.CreateServerPayloadAllOfNetworking{ - CreateServerNetworkingWithNics: &iaas.CreateServerNetworkingWithNics{ - NicIds: &[]string{"nic1", "nic2"}, - }, - }, - }, - isValid: true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdateServerPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - &iaas.UpdateServerPayload{ - Name: utils.Ptr("name"), - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -var _ serverControlClient = &mockServerControlClient{} - -// mockServerControlClient mocks the [serverControlClient] interface with -// pluggable functions -type mockServerControlClient struct { - wait.APIClientInterface - startServerCalled int - startServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error - - stopServerCalled int - stopServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error - - deallocateServerCalled int - deallocateServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) error - - getServerCalled int - getServerExecute func(callNo int, ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) -} - -// DeallocateServerExecute implements serverControlClient. -func (t *mockServerControlClient) DeallocateServerExecute(ctx context.Context, projectId, region, serverId string) error { - t.deallocateServerCalled++ - return t.deallocateServerExecute(t.deallocateServerCalled, ctx, projectId, region, serverId) -} - -// GetServerExecute implements serverControlClient. -func (t *mockServerControlClient) GetServerExecute(ctx context.Context, projectId, region, serverId string) (*iaas.Server, error) { - t.getServerCalled++ - return t.getServerExecute(t.getServerCalled, ctx, projectId, region, serverId) -} - -// StartServerExecute implements serverControlClient. -func (t *mockServerControlClient) StartServerExecute(ctx context.Context, projectId, region, serverId string) error { - t.startServerCalled++ - return t.startServerExecute(t.startServerCalled, ctx, projectId, region, serverId) -} - -// StopServerExecute implements serverControlClient. -func (t *mockServerControlClient) StopServerExecute(ctx context.Context, projectId, region, serverId string) error { - t.stopServerCalled++ - return t.stopServerExecute(t.stopServerCalled, ctx, projectId, region, serverId) -} - -func Test_serverResource_updateServerStatus(t *testing.T) { - projectId := basetypes.NewStringValue("projectId") - serverId := basetypes.NewStringValue("serverId") - type fields struct { - client *mockServerControlClient - } - type args struct { - currentState *string - model Model - region string - } - type want struct { - err bool - status types.String - getServerCount int - stopCount int - startCount int - deallocatedCount int - } - tests := []struct { - name string - fields fields - args args - want want - }{ - { - name: "no desired status", - fields: fields{ - client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { - return &iaas.Server{ - Id: utils.Ptr(serverId.ValueString()), - Status: utils.Ptr(wait.ServerActiveStatus), - }, nil - }, - }, - }, - args: args{ - currentState: utils.Ptr(wait.ServerActiveStatus), - model: Model{ - ProjectId: projectId, - ServerId: serverId, - }, - }, - want: want{ - getServerCount: 1, - }, - }, - - { - name: "desired inactive state", - fields: fields{ - client: &mockServerControlClient{ - getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) { - var state string - if no <= 1 { - state = wait.ServerActiveStatus - } else { - state = wait.ServerInactiveStatus - } - return &iaas.Server{ - Id: utils.Ptr(serverId.ValueString()), - Status: &state, - }, nil - }, - stopServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil }, - }, - }, - args: args{ - currentState: utils.Ptr(wait.ServerActiveStatus), - model: Model{ - ProjectId: projectId, - ServerId: serverId, - DesiredStatus: basetypes.NewStringValue("inactive"), - }, - }, - want: want{ - getServerCount: 2, - stopCount: 1, - status: basetypes.NewStringValue("inactive"), - }, - }, - { - name: "desired deallocated state", - fields: fields{ - client: &mockServerControlClient{ - getServerExecute: func(no int, _ context.Context, _, _, _ string) (*iaas.Server, error) { - var state string - switch no { - case 1: - state = wait.ServerActiveStatus - case 2: - state = wait.ServerInactiveStatus - default: - state = wait.ServerDeallocatedStatus - } - return &iaas.Server{ - Id: utils.Ptr(serverId.ValueString()), - Status: &state, - }, nil - }, - deallocateServerExecute: func(_ int, _ context.Context, _, _, _ string) error { return nil }, - }, - }, - args: args{ - currentState: utils.Ptr(wait.ServerActiveStatus), - model: Model{ - ProjectId: projectId, - ServerId: serverId, - DesiredStatus: basetypes.NewStringValue("deallocated"), - }, - }, - want: want{ - getServerCount: 3, - deallocatedCount: 1, - status: basetypes.NewStringValue("deallocated"), - }, - }, - { - name: "don't call start if active", - fields: fields{ - client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { - return &iaas.Server{ - Id: utils.Ptr(serverId.ValueString()), - Status: utils.Ptr(wait.ServerActiveStatus), - }, nil - }, - }, - }, - args: args{ - currentState: utils.Ptr(wait.ServerActiveStatus), - model: Model{ - ProjectId: projectId, - ServerId: serverId, - DesiredStatus: basetypes.NewStringValue("active"), - }, - }, - want: want{ - status: basetypes.NewStringValue("active"), - getServerCount: 1, - }, - }, - { - name: "don't call stop if inactive", - fields: fields{ - client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { - return &iaas.Server{ - Id: utils.Ptr(serverId.ValueString()), - Status: utils.Ptr(wait.ServerInactiveStatus), - }, nil - }, - }, - }, - args: args{ - currentState: utils.Ptr(wait.ServerInactiveStatus), - model: Model{ - ProjectId: projectId, - ServerId: serverId, - DesiredStatus: basetypes.NewStringValue("inactive"), - }, - }, - want: want{ - status: basetypes.NewStringValue("inactive"), - getServerCount: 1, - }, - }, - { - name: "don't call dealloacate if deallocated", - fields: fields{ - client: &mockServerControlClient{ - getServerExecute: func(_ int, _ context.Context, _, _, _ string) (*iaas.Server, error) { - return &iaas.Server{ - Id: utils.Ptr(serverId.ValueString()), - Status: utils.Ptr(wait.ServerDeallocatedStatus), - }, nil - }, - }, - }, - args: args{ - currentState: utils.Ptr(wait.ServerDeallocatedStatus), - model: Model{ - ProjectId: projectId, - ServerId: serverId, - DesiredStatus: basetypes.NewStringValue("deallocated"), - }, - }, - want: want{ - status: basetypes.NewStringValue("deallocated"), - getServerCount: 1, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model, tt.args.region) - if (err != nil) != tt.want.err { - t.Errorf("inconsistent error, want %v and got %v", tt.want.err, err) - } - if expected, actual := tt.want.status, tt.args.model.DesiredStatus; expected != actual { - t.Errorf("wanted status %s but got %s", expected, actual) - } - - if expected, actual := tt.want.getServerCount, tt.fields.client.getServerCalled; expected != actual { - t.Errorf("wrong number of get server calls: Expected %d but got %d", expected, actual) - } - if expected, actual := tt.want.startCount, tt.fields.client.startServerCalled; expected != actual { - t.Errorf("wrong number of start server calls: Expected %d but got %d", expected, actual) - } - if expected, actual := tt.want.stopCount, tt.fields.client.stopServerCalled; expected != actual { - t.Errorf("wrong number of stop server calls: Expected %d but got %d", expected, actual) - } - if expected, actual := tt.want.deallocatedCount, tt.fields.client.deallocateServerCalled; expected != actual { - t.Errorf("wrong number of deallocate server calls: Expected %d but got %d", expected, actual) - } - }) - } -} diff --git a/stackit/internal/services/iaas/serviceaccountattach/resource.go b/stackit/internal/services/iaas/serviceaccountattach/resource.go deleted file mode 100644 index 2063f15c..00000000 --- a/stackit/internal/services/iaas/serviceaccountattach/resource.go +++ /dev/null @@ -1,323 +0,0 @@ -package serviceaccountattach - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &serviceAccountAttachResource{} - _ resource.ResourceWithConfigure = &serviceAccountAttachResource{} - _ resource.ResourceWithImportState = &serviceAccountAttachResource{} - _ resource.ResourceWithModifyPlan = &serviceAccountAttachResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ServerId types.String `tfsdk:"server_id"` - ServiceAccountEmail types.String `tfsdk:"service_account_email"` -} - -// NewServiceAccountAttachResource is a helper function to simplify the provider implementation. -func NewServiceAccountAttachResource() resource.Resource { - return &serviceAccountAttachResource{} -} - -// serviceAccountAttachResource is the resource implementation. -type serviceAccountAttachResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *serviceAccountAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_service_account_attach" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *serviceAccountAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *serviceAccountAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *serviceAccountAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Service account attachment resource schema. Attaches a service account to a server. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`service_account_email`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the service account attachment is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "The server ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "service_account_email": schema.StringAttribute{ - Description: "The service account email.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *serviceAccountAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - - // Create new service account attachment - _, err := r.client.AddServiceAccountToServer(ctx, projectId, region, serverId, serviceAccountEmail).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching service account to server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, serviceAccountEmail) - model.Region = types.StringValue(region) - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Service account attachment created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *serviceAccountAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - - serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, region, serverId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account attachment", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if serviceAccounts == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account attachment", "List of service accounts attached to the server is nil") - return - } - - if serviceAccounts.Items != nil { - for _, mail := range *serviceAccounts.Items { - if mail != serviceAccountEmail { - continue - } - - model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, serviceAccountEmail) - model.Region = types.StringValue(region) - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Service account attachment read") - return - } - } - - // no matching service account was found, the attachment no longer exists - resp.State.RemoveResource(ctx) -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *serviceAccountAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update is not supported, all fields require replace -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *serviceAccountAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - service_accountId := model.ServiceAccountEmail.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "service_account_email", service_accountId) - - // Remove service_account from server - _, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, region, serverId, service_accountId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing service account from server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Service account attachment deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id -func (r *serviceAccountAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing service_account attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[service_account_email] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "server_id": idParts[2], - "service_account_email": idParts[3], - }) - - tflog.Info(ctx, "Service account attachment state imported") -} diff --git a/stackit/internal/services/iaas/testdata/datasource-image-v2-variants.tf b/stackit/internal/services/iaas/testdata/datasource-image-v2-variants.tf deleted file mode 100644 index a36deb55..00000000 --- a/stackit/internal/services/iaas/testdata/datasource-image-v2-variants.tf +++ /dev/null @@ -1,62 +0,0 @@ -variable "project_id" {} - -data "stackit_image_v2" "name_match_ubuntu_22_04" { - project_id = var.project_id - name = "Ubuntu 22.04" -} - -data "stackit_image_v2" "ubuntu_by_image_id" { - project_id = var.project_id - image_id = data.stackit_image_v2.name_match_ubuntu_22_04.image_id -} - -data "stackit_image_v2" "regex_match_ubuntu_22_04" { - project_id = var.project_id - name_regex = "(?i)^ubuntu 22.04$" -} - -data "stackit_image_v2" "filter_debian_11" { - project_id = var.project_id - filter = { - distro = "debian" - version = "11" - } -} - -data "stackit_image_v2" "filter_uefi_ubuntu" { - project_id = var.project_id - filter = { - distro = "ubuntu" - uefi = true - } -} - -data "stackit_image_v2" "name_regex_and_filter_rhel_9_1" { - project_id = var.project_id - name_regex = "^Red Hat Enterprise Linux 9.1$" - filter = { - distro = "rhel" - version = "9.1" - uefi = true - } -} - -data "stackit_image_v2" "name_windows_2022_standard" { - project_id = var.project_id - name = "Windows Server 2022 Standard" -} - -data "stackit_image_v2" "ubuntu_arm64_latest" { - project_id = var.project_id - filter = { - distro = "ubuntu-arm64" - } -} - -data "stackit_image_v2" "ubuntu_arm64_oldest" { - project_id = var.project_id - filter = { - distro = "ubuntu-arm64" - } - sort_ascending = true -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/datasource-machinetype.tf b/stackit/internal/services/iaas/testdata/datasource-machinetype.tf deleted file mode 100644 index 3475f34d..00000000 --- a/stackit/internal/services/iaas/testdata/datasource-machinetype.tf +++ /dev/null @@ -1,18 +0,0 @@ -variable "project_id" {} - -data "stackit_machine_type" "two_vcpus_filter" { - project_id = var.project_id - filter = "vcpus==2" -} - -data "stackit_machine_type" "filter_sorted_ascending_false" { - project_id = var.project_id - filter = "vcpus >= 2 && ram >= 2048" - sort_ascending = false -} - -# returns warning -data "stackit_machine_type" "no_match" { - project_id = var.project_id - filter = "vcpus == 99" -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/datasource-public-ip-ranges.tf b/stackit/internal/services/iaas/testdata/datasource-public-ip-ranges.tf deleted file mode 100644 index 6bdffaf1..00000000 --- a/stackit/internal/services/iaas/testdata/datasource-public-ip-ranges.tf +++ /dev/null @@ -1 +0,0 @@ -data "stackit_public_ip_ranges" "example" {} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-affinity-group-min.tf b/stackit/internal/services/iaas/testdata/resource-affinity-group-min.tf deleted file mode 100644 index 2e30e710..00000000 --- a/stackit/internal/services/iaas/testdata/resource-affinity-group-min.tf +++ /dev/null @@ -1,9 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "policy" {} - -resource "stackit_affinity_group" "affinity_group" { - project_id = var.project_id - name = var.name - policy = var.policy -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-image-max.tf b/stackit/internal/services/iaas/testdata/resource-image-max.tf deleted file mode 100644 index 83ec1d56..00000000 --- a/stackit/internal/services/iaas/testdata/resource-image-max.tf +++ /dev/null @@ -1,48 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "disk_format" {} -variable "local_file_path" {} -variable "min_disk_size" {} -variable "min_ram" {} -variable "label" {} -variable "boot_menu" {} -variable "cdrom_bus" {} -variable "disk_bus" {} -variable "nic_model" {} -variable "operating_system" {} -variable "operating_system_distro" {} -variable "operating_system_version" {} -variable "rescue_bus" {} -variable "rescue_device" {} -variable "secure_boot" {} -variable "uefi" {} -variable "video_model" {} -variable "virtio_scsi" {} - -resource "stackit_image" "image" { - project_id = var.project_id - name = var.name - disk_format = var.disk_format - local_file_path = var.local_file_path - min_disk_size = var.min_disk_size - min_ram = var.min_ram - labels = { - "acc-test" : var.label - } - config = { - boot_menu = var.boot_menu - cdrom_bus = var.cdrom_bus - disk_bus = var.disk_bus - nic_model = var.nic_model - operating_system = var.operating_system - operating_system_distro = var.operating_system_distro - operating_system_version = var.operating_system_version - rescue_bus = var.rescue_bus - rescue_device = var.rescue_device - secure_boot = var.secure_boot - uefi = var.uefi - video_model = var.video_model - virtio_scsi = var.virtio_scsi - } - -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-image-min.tf b/stackit/internal/services/iaas/testdata/resource-image-min.tf deleted file mode 100644 index ea7e843c..00000000 --- a/stackit/internal/services/iaas/testdata/resource-image-min.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "disk_format" {} -variable "local_file_path" {} - -resource "stackit_image" "image" { - project_id = var.project_id - name = var.name - disk_format = var.disk_format - local_file_path = var.local_file_path -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-key-pair-max.tf b/stackit/internal/services/iaas/testdata/resource-key-pair-max.tf deleted file mode 100644 index 281a5f84..00000000 --- a/stackit/internal/services/iaas/testdata/resource-key-pair-max.tf +++ /dev/null @@ -1,11 +0,0 @@ -variable "name" {} -variable "public_key" {} -variable "label" {} - -resource "stackit_key_pair" "key_pair" { - name = var.name - public_key = var.public_key - labels = { - "acc-test" : var.label - } -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-key-pair-min.tf b/stackit/internal/services/iaas/testdata/resource-key-pair-min.tf deleted file mode 100644 index 87d0adf1..00000000 --- a/stackit/internal/services/iaas/testdata/resource-key-pair-min.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "name" {} -variable "public_key" {} - -resource "stackit_key_pair" "key_pair" { - name = var.name - public_key = var.public_key -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-max.tf b/stackit/internal/services/iaas/testdata/resource-network-area-max.tf deleted file mode 100644 index 288fb0d0..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-area-max.tf +++ /dev/null @@ -1,49 +0,0 @@ -variable "organization_id" {} - -variable "name" {} -variable "transfer_network" {} -variable "network_ranges_prefix" {} -variable "default_nameservers" {} -variable "default_prefix_length" {} -variable "max_prefix_length" {} -variable "min_prefix_length" {} - -variable "route_destination_type" {} -variable "route_destination_value" {} -variable "route_next_hop_type" {} -variable "route_next_hop_value" {} -variable "label" {} - -resource "stackit_network_area" "network_area" { - organization_id = var.organization_id - name = var.name - network_ranges = [ - { - prefix = var.network_ranges_prefix - } - ] - transfer_network = var.transfer_network - default_nameservers = [var.default_nameservers] - default_prefix_length = var.default_prefix_length - max_prefix_length = var.max_prefix_length - min_prefix_length = var.min_prefix_length - labels = { - "acc-test" : var.label - } -} - -resource "stackit_network_area_route" "network_area_route" { - organization_id = stackit_network_area.network_area.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - destination = { - type = var.route_destination_type - value = var.route_destination_value - } - next_hop = { - type = var.route_next_hop_type - value = var.route_next_hop_value - } - labels = { - "acc-test" : var.label - } -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-min.tf b/stackit/internal/services/iaas/testdata/resource-network-area-min.tf deleted file mode 100644 index 5dde515d..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-area-min.tf +++ /dev/null @@ -1,8 +0,0 @@ -variable "organization_id" {} - -variable "name" {} - -resource "stackit_network_area" "network_area" { - organization_id = var.organization_id - name = var.name -} diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf b/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf deleted file mode 100644 index 1d207e45..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-area-region-max.tf +++ /dev/null @@ -1,33 +0,0 @@ -variable "organization_id" {} - -variable "name" {} -variable "transfer_network" {} -variable "network_ranges_prefix" {} -variable "default_prefix_length" {} -variable "min_prefix_length" {} -variable "max_prefix_length" {} -variable "default_nameservers" {} - -resource "stackit_network_area" "network_area" { - organization_id = var.organization_id - name = var.name -} - -resource "stackit_network_area_region" "network_area_region" { - organization_id = var.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - ipv4 = { - transfer_network = var.transfer_network - network_ranges = [ - { - prefix = var.network_ranges_prefix - } - ] - default_prefix_length = var.default_prefix_length - min_prefix_length = var.min_prefix_length - max_prefix_length = var.max_prefix_length - default_nameservers = [ - var.default_nameservers - ] - } -} diff --git a/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf b/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf deleted file mode 100644 index 19ebe100..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-area-region-min.tf +++ /dev/null @@ -1,23 +0,0 @@ -variable "organization_id" {} - -variable "name" {} -variable "transfer_network" {} -variable "network_ranges_prefix" {} - -resource "stackit_network_area" "network_area" { - organization_id = var.organization_id - name = var.name -} - -resource "stackit_network_area_region" "network_area_region" { - organization_id = var.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - ipv4 = { - transfer_network = var.transfer_network - network_ranges = [ - { - prefix = var.network_ranges_prefix - } - ] - } -} diff --git a/stackit/internal/services/iaas/testdata/resource-network-interface-max.tf b/stackit/internal/services/iaas/testdata/resource-network-interface-max.tf deleted file mode 100644 index f7b16f7d..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-interface-max.tf +++ /dev/null @@ -1,54 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "allowed_address" {} -variable "ipv4" {} -variable "ipv4_prefix" {} -variable "security" {} -variable "label" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.name - ipv4_prefix = var.ipv4_prefix -} - -resource "stackit_network_interface" "network_interface" { - project_id = var.project_id - network_id = stackit_network.network.network_id - name = var.name - allowed_addresses = var.security ? [var.allowed_address] : null - ipv4 = var.ipv4 - security = var.security - security_group_ids = var.security ? [stackit_security_group.security_group.security_group_id] : null - labels = { - "acc-test" : var.label - } -} - -resource "stackit_public_ip" "public_ip" { - project_id = var.project_id - network_interface_id = stackit_network_interface.network_interface.network_interface_id - labels = { - "acc-test" : var.label - } -} - -resource "stackit_network_interface" "network_interface_simple" { - project_id = var.project_id - network_id = stackit_network.network.network_id -} - -resource "stackit_public_ip" "public_ip_simple" { - project_id = var.project_id -} - -resource "stackit_public_ip_associate" "nic_public_ip_attach" { - project_id = var.project_id - network_interface_id = stackit_network_interface.network_interface_simple.network_interface_id - public_ip_id = stackit_public_ip.public_ip_simple.public_ip_id -} - -resource "stackit_security_group" "security_group" { - project_id = var.project_id - name = var.name -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-interface-min.tf b/stackit/internal/services/iaas/testdata/resource-network-interface-min.tf deleted file mode 100644 index 6a26db0d..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-interface-min.tf +++ /dev/null @@ -1,16 +0,0 @@ -variable "project_id" {} -variable "name" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.name -} - -resource "stackit_network_interface" "network_interface" { - project_id = var.project_id - network_id = stackit_network.network.network_id -} - -resource "stackit_public_ip" "public_ip" { - project_id = var.project_id -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-network-max.tf b/stackit/internal/services/iaas/testdata/resource-network-max.tf deleted file mode 100644 index 2d86028a..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-max.tf +++ /dev/null @@ -1,85 +0,0 @@ -variable "organization_id" {} -variable "name" {} -variable "ipv4_gateway" {} -variable "ipv4_nameserver_0" {} -variable "ipv4_nameserver_1" {} -variable "ipv4_prefix" {} -variable "ipv4_prefix_length" {} -variable "routed" {} -variable "label" {} -variable "service_account_mail" {} - -# no test candidate, just needed for the testing setup -resource "stackit_network_area" "network_area" { - organization_id = var.organization_id - name = var.name - labels = { - "preview/routingtables" = "true" - } -} - -resource "stackit_network_area_region" "network_area_region" { - organization_id = var.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - ipv4 = { - network_ranges = [ - { - prefix = "10.0.0.0/16" - }, - { - prefix = "10.2.2.0/24" - } - ] - transfer_network = "10.1.2.0/24" - } -} - -# no test candidate, just needed for the testing setup -resource "stackit_resourcemanager_project" "project" { - parent_container_id = stackit_network_area.network_area.organization_id - name = var.name - labels = { - "networkArea" = stackit_network_area.network_area.network_area_id - } - owner_email = var.service_account_mail - - depends_on = [stackit_network_area_region.network_area_region] -} - -resource "stackit_network" "network_prefix" { - project_id = stackit_resourcemanager_project.project.project_id - name = var.name - # ipv4_gateway = var.ipv4_gateway != "" ? var.ipv4_gateway : null - # no_ipv4_gateway = var.ipv4_gateway != "" ? null : true - ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] - ipv4_prefix = var.ipv4_prefix - routed = var.routed - labels = { - "acc-test" : var.label - } - - depends_on = [stackit_network_area_region.network_area_region] -} - -resource "stackit_network" "network_prefix_length" { - project_id = stackit_resourcemanager_project.project.project_id - name = var.name - # no_ipv4_gateway = true - ipv4_nameservers = [var.ipv4_nameserver_0, var.ipv4_nameserver_1] - ipv4_prefix_length = var.ipv4_prefix_length - routed = var.routed - labels = { - "acc-test" : var.label - } - routing_table_id = stackit_routing_table.routing_table.routing_table_id - - depends_on = [stackit_network.network_prefix, stackit_network_area_region.network_area_region] -} - -resource "stackit_routing_table" "routing_table" { - organization_id = var.organization_id - network_area_id = stackit_network_area.network_area.network_area_id - name = var.name - - depends_on = [stackit_network_area_region.network_area_region] -} diff --git a/stackit/internal/services/iaas/testdata/resource-network-min.tf b/stackit/internal/services/iaas/testdata/resource-network-min.tf deleted file mode 100644 index e2748bdd..00000000 --- a/stackit/internal/services/iaas/testdata/resource-network-min.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "project_id" {} -variable "name" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.name -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-security-group-max.tf b/stackit/internal/services/iaas/testdata/resource-security-group-max.tf deleted file mode 100644 index a7e8172d..00000000 --- a/stackit/internal/services/iaas/testdata/resource-security-group-max.tf +++ /dev/null @@ -1,72 +0,0 @@ -variable "project_id" {} - -variable "name" {} -variable "description" {} -variable "description_rule" {} -variable "label" {} -variable "stateful" {} -variable "direction" {} -variable "ether_type" {} -variable "ip_range" {} -variable "port" {} -variable "protocol" {} -variable "icmp_code" {} -variable "icmp_type" {} -variable "name_remote" {} - -resource "stackit_security_group" "security_group" { - project_id = var.project_id - name = var.name - description = var.description - labels = { - "acc-test" : var.label - } - stateful = var.stateful -} - -resource "stackit_security_group_rule" "security_group_rule" { - project_id = var.project_id - security_group_id = stackit_security_group.security_group.security_group_id - direction = var.direction - - description = var.description_rule - ether_type = var.ether_type - port_range = { - min = var.port - max = var.port - } - protocol = { - name = var.protocol - } - ip_range = var.ip_range -} - -resource "stackit_security_group_rule" "security_group_rule_icmp" { - project_id = var.project_id - security_group_id = stackit_security_group.security_group.security_group_id - direction = var.direction - - description = var.description_rule - ether_type = var.ether_type - icmp_parameters = { - code = var.icmp_code - type = var.icmp_type - } - protocol = { - name = "icmp" - } - ip_range = var.ip_range -} - -resource "stackit_security_group" "security_group_remote" { - project_id = var.project_id - name = var.name_remote -} - -resource "stackit_security_group_rule" "security_group_rule_remote_security_group" { - project_id = var.project_id - security_group_id = stackit_security_group.security_group.security_group_id - direction = var.direction - - remote_security_group_id = stackit_security_group.security_group_remote.security_group_id -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-security-group-min.tf b/stackit/internal/services/iaas/testdata/resource-security-group-min.tf deleted file mode 100644 index 7ae5f6f3..00000000 --- a/stackit/internal/services/iaas/testdata/resource-security-group-min.tf +++ /dev/null @@ -1,15 +0,0 @@ -variable "project_id" {} - -variable "name" {} -variable "direction" {} - -resource "stackit_security_group" "security_group" { - project_id = var.project_id - name = var.name -} - -resource "stackit_security_group_rule" "security_group_rule" { - project_id = var.project_id - security_group_id = stackit_security_group.security_group.security_group_id - direction = var.direction -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-server-max-server-attachments.tf b/stackit/internal/services/iaas/testdata/resource-server-max-server-attachments.tf deleted file mode 100644 index d9902e95..00000000 --- a/stackit/internal/services/iaas/testdata/resource-server-max-server-attachments.tf +++ /dev/null @@ -1,11 +0,0 @@ -resource "stackit_server_volume_attach" "data_volume_attachment" { - project_id = var.project_id - server_id = stackit_server.server.server_id - volume_id = stackit_volume.data_volume.volume_id -} - -resource "stackit_server_network_interface_attach" "network_interface_second_attachment" { - project_id = var.project_id - network_interface_id = stackit_network_interface.network_interface_second.network_interface_id - server_id = stackit_server.server.server_id -} diff --git a/stackit/internal/services/iaas/testdata/resource-server-max.tf b/stackit/internal/services/iaas/testdata/resource-server-max.tf deleted file mode 100644 index 4150bcc2..00000000 --- a/stackit/internal/services/iaas/testdata/resource-server-max.tf +++ /dev/null @@ -1,82 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "name_not_updated" {} -variable "machine_type" {} -variable "image_id" {} -variable "availability_zone" {} -variable "label" {} -variable "user_data" {} -variable "desired_status" {} - -variable "policy" {} -variable "size" {} -variable "public_key" {} -variable "service_account_mail" {} - -resource "stackit_affinity_group" "affinity_group" { - project_id = var.project_id - name = var.name_not_updated - policy = var.policy -} - -resource "stackit_volume" "base_volume" { - project_id = var.project_id - availability_zone = var.availability_zone - size = var.size - source = { - id = var.image_id - type = "image" - } -} - -resource "stackit_volume" "data_volume" { - project_id = var.project_id - availability_zone = var.availability_zone - size = var.size -} - - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.name -} - -resource "stackit_network_interface" "network_interface_init" { - project_id = var.project_id - network_id = stackit_network.network.network_id -} - -resource "stackit_network_interface" "network_interface_second" { - project_id = var.project_id - network_id = stackit_network.network.network_id -} - -resource "stackit_key_pair" "key_pair" { - name = var.name_not_updated - public_key = var.public_key -} - -resource "stackit_server_service_account_attach" "attached_service_account" { - project_id = var.project_id - server_id = stackit_server.server.server_id - service_account_email = var.service_account_mail -} - -resource "stackit_server" "server" { - project_id = var.project_id - name = var.name - machine_type = var.machine_type - affinity_group = stackit_affinity_group.affinity_group.affinity_group_id - availability_zone = var.availability_zone - keypair_name = stackit_key_pair.key_pair.name - desired_status = var.desired_status - network_interfaces = [stackit_network_interface.network_interface_init.network_interface_id] - user_data = var.user_data - boot_volume = { - source_type = "volume" - source_id = stackit_volume.base_volume.volume_id - } - labels = { - "acc-test" : var.label - } -} diff --git a/stackit/internal/services/iaas/testdata/resource-server-min.tf b/stackit/internal/services/iaas/testdata/resource-server-min.tf deleted file mode 100644 index 6f3ba894..00000000 --- a/stackit/internal/services/iaas/testdata/resource-server-min.tf +++ /dev/null @@ -1,30 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "network_name" {} -variable "machine_type" {} -variable "image_id" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.network_name -} - -resource "stackit_network_interface" "nic" { - project_id = var.project_id - network_id = stackit_network.network.network_id -} - -resource "stackit_server" "server" { - project_id = var.project_id - name = var.name - machine_type = var.machine_type - boot_volume = { - source_type = "image" - size = 16 - source_id = var.image_id - delete_on_termination = true - } - network_interfaces = [ - stackit_network_interface.nic.network_interface_id - ] -} diff --git a/stackit/internal/services/iaas/testdata/resource-volume-max.tf b/stackit/internal/services/iaas/testdata/resource-volume-max.tf deleted file mode 100644 index 54c590f6..00000000 --- a/stackit/internal/services/iaas/testdata/resource-volume-max.tf +++ /dev/null @@ -1,36 +0,0 @@ -variable "project_id" {} -variable "availability_zone" {} -variable "name" {} -variable "size" {} -variable "description" {} -variable "performance_class" {} -variable "label" {} - -resource "stackit_volume" "volume_size" { - project_id = var.project_id - availability_zone = var.availability_zone - name = var.name - size = var.size - description = var.description - performance_class = var.performance_class - labels = { - "acc-test" : var.label - } -} - -resource "stackit_volume" "volume_source" { - project_id = var.project_id - availability_zone = var.availability_zone - name = var.name - description = var.description - # TODO: keep commented until IaaS API bug is resolved - #performance_class = var.performance_class - size = var.size - source = { - id = stackit_volume.volume_size.volume_id - type = "volume" - } - labels = { - "acc-test" : var.label - } -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/testdata/resource-volume-min.tf b/stackit/internal/services/iaas/testdata/resource-volume-min.tf deleted file mode 100644 index bd114676..00000000 --- a/stackit/internal/services/iaas/testdata/resource-volume-min.tf +++ /dev/null @@ -1,18 +0,0 @@ -variable "project_id" {} -variable "availability_zone" {} -variable "size" {} - -resource "stackit_volume" "volume_size" { - project_id = var.project_id - availability_zone = var.availability_zone - size = var.size -} - -resource "stackit_volume" "volume_source" { - project_id = var.project_id - availability_zone = var.availability_zone - source = { - id = stackit_volume.volume_size.volume_id - type = "volume" - } -} \ No newline at end of file diff --git a/stackit/internal/services/iaas/utils/util.go b/stackit/internal/services/iaas/utils/util.go deleted file mode 100644 index 79368cf4..00000000 --- a/stackit/internal/services/iaas/utils/util.go +++ /dev/null @@ -1,52 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *iaas.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.IaaSCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IaaSCustomEndpoint)) - } - - apiClient, err := iaas.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} - -func MapLabels(ctx context.Context, responseLabels *map[string]interface{}, currentLabels types.Map) (basetypes.MapValue, error) { //nolint:gocritic // Linter wants to have a non-pointer type for the map, but this would mean a nil check has to be done before every usage of this func. - labelsTF, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{}) - if diags.HasError() { - return labelsTF, fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags)) - } - - if responseLabels != nil && len(*responseLabels) != 0 { - var diags diag.Diagnostics - labelsTF, diags = types.MapValueFrom(ctx, types.StringType, *responseLabels) - if diags.HasError() { - return labelsTF, fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags)) - } - } else if currentLabels.IsNull() { - labelsTF = types.MapNull(types.StringType) - } - - return labelsTF, nil -} diff --git a/stackit/internal/services/iaas/utils/util_test.go b/stackit/internal/services/iaas/utils/util_test.go deleted file mode 100644 index 79af1174..00000000 --- a/stackit/internal/services/iaas/utils/util_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://iaas-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *iaas.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *iaas.APIClient { - apiClient, err := iaas.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - IaaSCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *iaas.APIClient { - apiClient, err := iaas.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} - -func TestMapLabels(t *testing.T) { - type args struct { - responseLabels *map[string]interface{} - currentLabels types.Map - } - tests := []struct { - name string - args args - want basetypes.MapValue - wantErr bool - }{ - { - name: "response labels is set", - args: args{ - responseLabels: &map[string]interface{}{ - "foo1": "bar1", - "foo2": "bar2", - }, - currentLabels: types.MapUnknown(types.StringType), - }, - wantErr: false, - want: types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo1": types.StringValue("bar1"), - "foo2": types.StringValue("bar2"), - }), - }, - { - name: "response labels is set but empty", - args: args{ - responseLabels: &map[string]interface{}{}, - currentLabels: types.MapUnknown(types.StringType), - }, - wantErr: false, - want: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - { - name: "response labels is nil and model labels is nil", - args: args{ - responseLabels: nil, - currentLabels: types.MapNull(types.StringType), - }, - wantErr: false, - want: types.MapNull(types.StringType), - }, - { - name: "response labels is nil and model labels is set", - args: args{ - responseLabels: nil, - currentLabels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo1": types.StringValue("bar1"), - "foo2": types.StringValue("bar2"), - }), - }, - wantErr: false, - want: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - { - name: "response labels is nil and model labels is set but empty", - args: args{ - responseLabels: nil, - currentLabels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - wantErr: false, - want: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := MapLabels(ctx, tt.args.responseLabels, tt.args.currentLabels) - if (err != nil) != tt.wantErr { - t.Errorf("MapLabels() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("MapLabels() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/stackit/internal/services/iaas/volume/datasource.go b/stackit/internal/services/iaas/volume/datasource.go deleted file mode 100644 index 5e36a395..00000000 --- a/stackit/internal/services/iaas/volume/datasource.go +++ /dev/null @@ -1,188 +0,0 @@ -package volume - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &volumeDataSource{} -) - -// NewVolumeDataSource is a helper function to simplify the provider implementation. -func NewVolumeDataSource() datasource.DataSource { - return &volumeDataSource{} -} - -// volumeDataSource is the data source implementation. -type volumeDataSource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *volumeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_volume" -} - -func (d *volumeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (d *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Volume resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the volume is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "volume_id": schema.StringAttribute{ - Description: "The volume ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "The server ID of the server to which the volume is attached to.", - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the volume.", - Computed: true, - }, - "description": schema.StringAttribute{ - Description: "The description of the volume.", - Computed: true, - }, - "availability_zone": schema.StringAttribute{ - Description: "The availability zone of the volume.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - "performance_class": schema.StringAttribute{ - MarkdownDescription: "The performance class of the volume. Possible values are documented in [Service plans BlockStorage](https://docs.stackit.cloud/products/storage/block-storage/basics/service-plans/#currently-available-service-plans-performance-classes)", - Computed: true, - }, - "size": schema.Int64Attribute{ - Description: "The size of the volume in GB. It can only be updated to a larger value than the current size", - Computed: true, - }, - "source": schema.SingleNestedAttribute{ - Description: "The source of the volume. It can be either a volume, an image, a snapshot or a backup", - Computed: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: "The type of the source. " + utils.FormatPossibleValues(SupportedSourceTypes...), - Computed: true, - }, - "id": schema.StringAttribute{ - Description: "The ID of the source, e.g. image ID", - Computed: true, - }, - }, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - volumeId := model.VolumeId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - volumeResp, err := d.client.GetVolume(ctx, projectId, region, volumeId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading volume", - fmt.Sprintf("Volume with ID %q does not exist in project %q.", volumeId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, volumeResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "volume read") -} diff --git a/stackit/internal/services/iaas/volume/resource.go b/stackit/internal/services/iaas/volume/resource.go deleted file mode 100644 index 0fc3a9e6..00000000 --- a/stackit/internal/services/iaas/volume/resource.go +++ /dev/null @@ -1,672 +0,0 @@ -package volume - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "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/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &volumeResource{} - _ resource.ResourceWithConfigure = &volumeResource{} - _ resource.ResourceWithImportState = &volumeResource{} - _ resource.ResourceWithModifyPlan = &volumeResource{} - - SupportedSourceTypes = []string{"volume", "image", "snapshot", "backup"} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - VolumeId types.String `tfsdk:"volume_id"` - Name types.String `tfsdk:"name"` - AvailabilityZone types.String `tfsdk:"availability_zone"` - Labels types.Map `tfsdk:"labels"` - Description types.String `tfsdk:"description"` - PerformanceClass types.String `tfsdk:"performance_class"` - Size types.Int64 `tfsdk:"size"` - ServerId types.String `tfsdk:"server_id"` - Source types.Object `tfsdk:"source"` -} - -// Struct corresponding to Model.Source -type sourceModel struct { - Type types.String `tfsdk:"type"` - Id types.String `tfsdk:"id"` -} - -// Types corresponding to sourceModel -var sourceTypes = map[string]attr.Type{ - "type": basetypes.StringType{}, - "id": basetypes.StringType{}, -} - -// NewVolumeResource is a helper function to simplify the provider implementation. -func NewVolumeResource() resource.Resource { - return &volumeResource{} -} - -// volumeResource is the resource implementation. -type volumeResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *volumeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_volume" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *volumeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// ConfigValidators validates the resource configuration -func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { - return []resource.ConfigValidator{ - resourcevalidator.AtLeastOneOf( - path.MatchRoot("source"), - path.MatchRoot("size"), - ), - } -} - -// Configure adds the provider configured client to the resource. -func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Volume resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`volume_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the volume is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "volume_id": schema.StringAttribute{ - Description: "The volume ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "The server ID of the server to which the volume is attached to.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the volume.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "description": schema.StringAttribute{ - Description: "The description of the volume.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(127), - }, - }, - "availability_zone": schema.StringAttribute{ - Description: "The availability zone of the volume.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Required: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - "performance_class": schema.StringAttribute{ - MarkdownDescription: "The performance class of the volume. Possible values are documented in [Service plans BlockStorage](https://docs.stackit.cloud/products/storage/block-storage/basics/service-plans/#currently-available-service-plans-performance-classes)", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`), - "must match expression"), - }, - }, - "size": schema.Int64Attribute{ - Description: "The size of the volume in GB. It can only be updated to a larger value than the current size. Either `size` or `source` must be provided", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - volumeResizeModifier{}, - }, - }, - "source": schema.SingleNestedAttribute{ - Description: "The source of the volume. It can be either a volume, an image, a snapshot or a backup. Either `size` or `source` must be provided", - Optional: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.RequiresReplace(), - }, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: "The type of the source. " + utils.FormatPossibleValues(SupportedSourceTypes...), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "id": schema.StringAttribute{ - Description: "The ID of the source, e.g. image ID", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - }, - }, - } -} - -var _ planmodifier.Int64 = volumeResizeModifier{} - -type volumeResizeModifier struct { -} - -// Description implements planmodifier.String. -func (v volumeResizeModifier) Description(context.Context) string { - return "validates volume resize" -} - -// MarkdownDescription implements planmodifier.String. -func (v volumeResizeModifier) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -// PlanModifyInt64 implements planmodifier.Int64. -func (v volumeResizeModifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { // nolint:gocritic // function signature required by Terraform - var planSize types.Int64 - var currentSize types.Int64 - - resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("size"), &planSize)...) - if resp.Diagnostics.HasError() { - return - } - resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("size"), ¤tSize)...) - if resp.Diagnostics.HasError() { - return - } - if planSize.ValueInt64() < currentSize.ValueInt64() { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error changing volume size", "A volume cannot be made smaller in order to prevent data loss.") - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - var source = &sourceModel{} - if !(model.Source.IsNull() || model.Source.IsUnknown()) { - diags = model.Source.As(ctx, source, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model, source) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new volume - - volume, err := r.client.CreateVolume(ctx, projectId, region).CreateVolumePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - volumeId := *volume.Id - volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("volume creation waiting: %v", err)) - return - } - - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - // Map response body to schema - err = mapFields(ctx, volume, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Volume created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - volumeId := model.VolumeId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - volumeResp, err := r.client.GetVolume(ctx, projectId, region, volumeId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, volumeResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "volume read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - volumeId := model.VolumeId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing volume - updatedVolume, err := r.client.UpdateVolume(ctx, projectId, region, volumeId).UpdateVolumePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Resize existing volume - modelSize := conversion.Int64ValueToPointer(model.Size) - if modelSize != nil && updatedVolume.Size != nil { - // A volume can only be resized to larger values, otherwise an error occurs - if *modelSize < *updatedVolume.Size { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("The new volume size must be larger than the current size (%d GB)", *updatedVolume.Size)) - } else if *modelSize > *updatedVolume.Size { - payload := iaas.ResizeVolumePayload{ - Size: modelSize, - } - err := r.client.ResizeVolume(ctx, projectId, region, volumeId).ResizeVolumePayload(payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Resizing the volume, calling API: %v", err)) - } - // Update volume model because the API doesn't return a volume object as response - updatedVolume.Size = modelSize - } - } - err = mapFields(ctx, updatedVolume, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "volume updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - volumeId := model.VolumeId.ValueString() - - ctx = core.InitProviderContext(ctx) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - // Delete existing volume - err := r.client.DeleteVolume(ctx, projectId, region, volumeId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, region, volumeId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("volume deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "volume deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,volume_id -func (r *volumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing volume", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[volume_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "volume_id": idParts[2], - }) - - tflog.Info(ctx, "volume state imported") -} - -func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model, region string) error { - if volumeResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var volumeId string - if model.VolumeId.ValueString() != "" { - volumeId = model.VolumeId.ValueString() - } else if volumeResp.Id != nil { - volumeId = *volumeResp.Id - } else { - return fmt.Errorf("Volume id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, volumeId) - model.Region = types.StringValue(region) - - labels, err := iaasUtils.MapLabels(ctx, volumeResp.Labels, model.Labels) - if err != nil { - return err - } - - var sourceValues map[string]attr.Value - var sourceObject basetypes.ObjectValue - if volumeResp.Source == nil { - sourceObject = types.ObjectNull(sourceTypes) - } else { - sourceValues = map[string]attr.Value{ - "type": types.StringPointerValue(volumeResp.Source.Type), - "id": types.StringPointerValue(volumeResp.Source.Id), - } - var diags diag.Diagnostics - sourceObject, diags = types.ObjectValue(sourceTypes, sourceValues) - if diags.HasError() { - return fmt.Errorf("creating source: %w", core.DiagsToError(diags)) - } - } - - model.VolumeId = types.StringValue(volumeId) - model.AvailabilityZone = types.StringPointerValue(volumeResp.AvailabilityZone) - model.Description = types.StringPointerValue(volumeResp.Description) - model.Name = types.StringPointerValue(volumeResp.Name) - // Workaround for volumes with no names which return an empty string instead of nil - if name := volumeResp.Name; name != nil && *name == "" { - model.Name = types.StringNull() - } - model.Labels = labels - model.PerformanceClass = types.StringPointerValue(volumeResp.PerformanceClass) - model.ServerId = types.StringPointerValue(volumeResp.ServerId) - model.Size = types.Int64PointerValue(volumeResp.Size) - model.Source = sourceObject - return nil -} - -func toCreatePayload(ctx context.Context, model *Model, source *sourceModel) (*iaas.CreateVolumePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - var sourcePayload *iaas.VolumeSource - - if !source.Id.IsNull() && !source.Type.IsNull() { - sourcePayload = &iaas.VolumeSource{ - Id: conversion.StringValueToPointer(source.Id), - Type: conversion.StringValueToPointer(source.Type), - } - } - - return &iaas.CreateVolumePayload{ - AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone), - Description: conversion.StringValueToPointer(model.Description), - Labels: &labels, - Name: conversion.StringValueToPointer(model.Name), - PerformanceClass: conversion.StringValueToPointer(model.PerformanceClass), - Size: conversion.Int64ValueToPointer(model.Size), - Source: sourcePayload, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateVolumePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaas.UpdateVolumePayload{ - Description: conversion.StringValueToPointer(model.Description), - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - }, nil -} diff --git a/stackit/internal/services/iaas/volume/resource_test.go b/stackit/internal/services/iaas/volume/resource_test.go deleted file mode 100644 index 14f456a7..00000000 --- a/stackit/internal/services/iaas/volume/resource_test.go +++ /dev/null @@ -1,268 +0,0 @@ -package volume - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *iaas.Volume - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - }, - input: &iaas.Volume{ - Id: utils.Ptr("nid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,nid"), - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - Name: types.StringNull(), - AvailabilityZone: types.StringNull(), - Labels: types.MapNull(types.StringType), - Description: types.StringNull(), - PerformanceClass: types.StringNull(), - ServerId: types.StringNull(), - Size: types.Int64Null(), - Source: types.ObjectNull(sourceTypes), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "simple_values", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - Region: types.StringValue("eu01"), - }, - input: &iaas.Volume{ - Id: utils.Ptr("nid"), - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Description: utils.Ptr("desc"), - PerformanceClass: utils.Ptr("class"), - ServerId: utils.Ptr("sid"), - Size: utils.Ptr(int64(1)), - Source: &iaas.VolumeSource{}, - }, - region: "eu02", - }, - expected: Model{ - Id: types.StringValue("pid,eu02,nid"), - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - Name: types.StringValue("name"), - AvailabilityZone: types.StringValue("zone"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Description: types.StringValue("desc"), - PerformanceClass: types.StringValue("class"), - ServerId: types.StringValue("sid"), - Size: types.Int64Value(1), - Source: types.ObjectValueMust(sourceTypes, map[string]attr.Value{ - "type": types.StringNull(), - "id": types.StringNull(), - }), - Region: types.StringValue("eu02"), - }, - isValid: true, - }, - { - description: "empty_labels", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - input: &iaas.Volume{ - Id: utils.Ptr("nid"), - }, - region: "eu01", - }, - expected: Model{ - Id: types.StringValue("pid,eu01,nid"), - ProjectId: types.StringValue("pid"), - VolumeId: types.StringValue("nid"), - Name: types.StringNull(), - AvailabilityZone: types.StringNull(), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}), - Description: types.StringNull(), - PerformanceClass: types.StringNull(), - ServerId: types.StringNull(), - Size: types.Int64Null(), - Source: types.ObjectNull(sourceTypes), - Region: types.StringValue("eu01"), - }, - isValid: true, - }, - { - description: "response_nil_fail", - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - ProjectId: types.StringValue("pid"), - }, - input: &iaas.Volume{}, - }, - expected: Model{}, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.args.input, &tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - source *sourceModel - expected *iaas.CreateVolumePayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - AvailabilityZone: types.StringValue("zone"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Description: types.StringValue("desc"), - PerformanceClass: types.StringValue("class"), - Size: types.Int64Value(1), - Source: types.ObjectValueMust(sourceTypes, map[string]attr.Value{ - "type": types.StringNull(), - "id": types.StringNull(), - }), - }, - &sourceModel{ - Type: types.StringValue("volume"), - Id: types.StringValue("id"), - }, - &iaas.CreateVolumePayload{ - Name: utils.Ptr("name"), - AvailabilityZone: utils.Ptr("zone"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Description: utils.Ptr("desc"), - PerformanceClass: utils.Ptr("class"), - Size: utils.Ptr(int64(1)), - Source: &iaas.VolumeSource{ - Type: utils.Ptr("volume"), - Id: utils.Ptr("id"), - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input, tt.source) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaas.UpdateVolumePayload - isValid bool - }{ - { - "default_ok", - &Model{ - Name: types.StringValue("name"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - Description: types.StringValue("desc"), - }, - &iaas.UpdateVolumePayload{ - Name: utils.Ptr("name"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Description: utils.Ptr("desc"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaas/volumeattach/resource.go b/stackit/internal/services/iaas/volumeattach/resource.go deleted file mode 100644 index f297f16d..00000000 --- a/stackit/internal/services/iaas/volumeattach/resource.go +++ /dev/null @@ -1,325 +0,0 @@ -package volumeattach - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &volumeAttachResource{} - _ resource.ResourceWithConfigure = &volumeAttachResource{} - _ resource.ResourceWithImportState = &volumeAttachResource{} - _ resource.ResourceWithModifyPlan = &volumeAttachResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - ServerId types.String `tfsdk:"server_id"` - VolumeId types.String `tfsdk:"volume_id"` -} - -// NewVolumeAttachResource is a helper function to simplify the provider implementation. -func NewVolumeAttachResource() resource.Resource { - return &volumeAttachResource{} -} - -// volumeAttachResource is the resource implementation. -type volumeAttachResource struct { - client *iaas.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *volumeAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_volume_attach" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *volumeAttachResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *volumeAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "iaas client configured") -} - -// Schema defines the schema for the resource. -func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Volume attachment resource schema. Attaches a volume to a server. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - MarkdownDescription: description, - Description: description, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`server_id`,`volume_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the volume attachment is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "The server ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "volume_id": schema.StringAttribute{ - Description: "The volume ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - volumeId := model.VolumeId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - // Create new Volume attachment - - payload := iaas.AddVolumeToServerPayload{ - DeleteOnTermination: sdkUtils.Ptr(false), - } - _, err := r.client.AddVolumeToServer(ctx, projectId, region, serverId, volumeId).AddVolumeToServerPayload(payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("volume attachment waiting: %v", err)) - return - } - - model.Id = utils.BuildInternalTerraformId(projectId, region, serverId, volumeId) - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Volume attachment created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *volumeAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - volumeId := model.VolumeId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - _, err := r.client.GetAttachedVolume(ctx, projectId, region, serverId, volumeId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume attachment", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Volume attachment read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *volumeAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update is not supported, all fields require replace -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - serverId := model.ServerId.ValueString() - volumeId := model.VolumeId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "volume_id", volumeId) - - // Remove volume from server - err := r.client.RemoveVolumeFromServer(ctx, projectId, region, serverId, volumeId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, region, serverId, volumeId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("volume removal waiting: %v", err)) - return - } - - tflog.Info(ctx, "Volume attachment deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id -func (r *volumeAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing volume attachment", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[server_id],[volume_id] Got: %q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "server_id": idParts[2], - "volume_id": idParts[3], - }) - - tflog.Info(ctx, "Volume attachment state imported") -} diff --git a/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go b/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go deleted file mode 100644 index dd0e0654..00000000 --- a/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go +++ /dev/null @@ -1,831 +0,0 @@ -package iaasalpha_test - -import ( - "context" - _ "embed" - "errors" - "fmt" - "maps" - "net/http" - "strings" - "sync" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// TODO: create network area using terraform resource instead once it's out of experimental stage and GA -const ( - testNetworkAreaId = "25bbf23a-8134-4439-9f5e-1641caf8354e" -) - -var ( - //go:embed testdata/resource-routingtable-min.tf - resourceRoutingTableMinConfig string - - //go:embed testdata/resource-routingtable-max.tf - resourceRoutingTableMaxConfig string - - //go:embed testdata/resource-routingtable-route-min.tf - resourceRoutingTableRouteMinConfig string - - //go:embed testdata/resource-routingtable-route-max.tf - resourceRoutingTableRouteMaxConfig string -) - -var testConfigRoutingTableMin = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "network_area_id": config.StringVariable(testNetworkAreaId), - "name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), -} - -var testConfigRoutingTableMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigRoutingTableMin) - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))) - return updatedConfig -}() - -var testConfigRoutingTableMax = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "network_area_id": config.StringVariable(testNetworkAreaId), - "name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "description": config.StringVariable("This is the description of the routing table."), - "label": config.StringVariable("routing-table-label-01"), - "system_routes": config.BoolVariable(false), - "region": config.StringVariable(testutil.Region), -} - -var testConfigRoutingTableMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigRoutingTableMax { - updatedConfig[k] = v - } - updatedConfig["name"] = config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))) - updatedConfig["description"] = config.StringVariable("This is the updated description of the routing table.") - updatedConfig["label"] = config.StringVariable("routing-table-updated-label-01") - return updatedConfig -}() - -var testConfigRoutingTableRouteMin = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "network_area_id": config.StringVariable(testNetworkAreaId), - "routing_table_name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "destination_type": config.StringVariable("cidrv4"), - "destination_value": config.StringVariable("192.168.178.0/24"), - "next_hop_type": config.StringVariable("ipv4"), - "next_hop_value": config.StringVariable("192.168.178.1"), -} - -var testConfigRoutingTableRouteMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigRoutingTableRouteMin) - // nothing possible to update of the required attributes... - return updatedConfig -}() - -var testConfigRoutingTableRouteMax = config.Variables{ - "organization_id": config.StringVariable(testutil.OrganizationId), - "network_area_id": config.StringVariable(testNetworkAreaId), - "routing_table_name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "destination_type": config.StringVariable("cidrv4"), // TODO: use cidrv6 once it's supported as we already test cidrv4 in the min test - "destination_value": config.StringVariable("192.168.178.0/24"), - "next_hop_type": config.StringVariable("ipv4"), // TODO: use ipv6, internet or blackhole once they are supported as we already test ipv4 in the min test - "next_hop_value": config.StringVariable("192.168.178.1"), - "label": config.StringVariable("route-label-01"), -} - -var testConfigRoutingTableRouteMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigRoutingTableRouteMax) - updatedConfig["label"] = config.StringVariable("route-updated-label-01") - return updatedConfig -}() - -// execute routingtable and routingtable route min and max tests with t.Run() to prevent parallel runs (needed for tests of stackit_routing_tables datasource) -func TestAccRoutingTable(t *testing.T) { - t.Run("TestAccRoutingTableMin", func(t *testing.T) { - t.Logf("TestAccRoutingTableMin name: %s", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigRoutingTableMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), - resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Data sources - { - ConfigVariables: testConfigRoutingTableMin, - Config: fmt.Sprintf(` - %s - %s - - # single routing table - data "stackit_routing_table" "routing_table" { - organization_id = stackit_routing_table.routing_table.organization_id - network_area_id = stackit_routing_table.routing_table.network_area_id - routing_table_id = stackit_routing_table.routing_table.routing_table_id - } - - # all routing tables in network area - data "stackit_routing_tables" "routing_tables" { - organization_id = stackit_routing_table.routing_table.organization_id - network_area_id = stackit_routing_table.routing_table.network_area_id - } - `, - testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["network_area_id"])), - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "data.stackit_routing_table.routing_table", "routing_table_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "0"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.Region), - resource.TestCheckNoResourceAttr("data.stackit_routing_table.routing_table", "description"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "system_routes", "true"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "default", "false"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "updated_at"), - - // Routing tables - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["network_area_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "region", testutil.Region), - // there will be always two routing tables because of the main routing table of the network area - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.#", "2"), - - // default routing table - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.0.default", "true"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.updated_at"), - - // second routing table managed via terraform - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "data.stackit_routing_tables.routing_tables", "items.1.routing_table_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.name", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.labels.%", "0"), - resource.TestCheckNoResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.description"), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.system_routes", "true"), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.default", "false"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigRoutingTableMinUpdated, - ResourceName: "stackit_routing_table.routing_table", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_routing_table.routing_table"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_routing_table.routing_table") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - routingTableId, ok := r.Primary.Attributes["routing_table_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute routing_table_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigRoutingTableMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMinUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMinUpdated["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMinUpdated["name"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), - resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) - }) - - t.Run("TestAccRoutingTableMax", func(t *testing.T) { - t.Logf("TestAccRoutingTableMax name: %s", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigRoutingTableMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMax["label"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMax["region"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "description", testutil.ConvertConfigVariable(testConfigRoutingTableMax["description"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMax["system_routes"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Data sources - { - ConfigVariables: testConfigRoutingTableMax, - Config: fmt.Sprintf(` - %s - %s - - # single routing table - data "stackit_routing_table" "routing_table" { - organization_id = stackit_routing_table.routing_table.organization_id - network_area_id = stackit_routing_table.routing_table.network_area_id - routing_table_id = stackit_routing_table.routing_table.routing_table_id - } - - # all routing tables in network area - data "stackit_routing_tables" "routing_tables" { - organization_id = stackit_routing_table.routing_table.organization_id - network_area_id = stackit_routing_table.routing_table.network_area_id - } - `, - testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["network_area_id"])), - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "data.stackit_routing_table.routing_table", "routing_table_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMax["label"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMax["region"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "description", testutil.ConvertConfigVariable(testConfigRoutingTableMax["description"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMax["system_routes"])), - resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "default", "false"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "updated_at"), - - // Routing tables - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["network_area_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMax["region"])), - // there will be always two routing tables because of the main routing table of the network area - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.#", "2"), - - // default routing table - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.0.default", "true"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.updated_at"), - - // second routing table managed via terraform - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "data.stackit_routing_tables.routing_tables", "items.1.routing_table_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.name", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMax["label"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.description", testutil.ConvertConfigVariable(testConfigRoutingTableMax["description"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMax["system_routes"])), - resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.default", "false"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigRoutingTableMaxUpdated, - ResourceName: "stackit_routing_table.routing_table", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_routing_table.routing_table"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_routing_table.routing_table") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - routingTableId, ok := r.Primary.Attributes["routing_table_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute routing_table_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigRoutingTableMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["name"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["label"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["region"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "description", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["description"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["system_routes"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) - }) - - t.Run("TestAccRoutingTableRouteMin", func(t *testing.T) { - t.Logf("TestAccRoutingTableRouteMin") - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigRoutingTableRouteMin, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["routing_table_name"])), - - // Routing table route - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "stackit_routing_table_route.route", "routing_table_id", - ), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "0"), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), - ), - }, - // Data sources - { - ConfigVariables: testConfigRoutingTableRouteMin, - Config: fmt.Sprintf(` - %s - %s - - # single routing table route - data "stackit_routing_table_route" "route" { - organization_id = stackit_routing_table_route.route.organization_id - network_area_id = stackit_routing_table_route.route.network_area_id - routing_table_id = stackit_routing_table_route.route.routing_table_id - route_id = stackit_routing_table_route.route.route_id - } - - # all routing table routes in routing table - data "stackit_routing_table_routes" "routes" { - organization_id = stackit_routing_table_route.route.organization_id - network_area_id = stackit_routing_table_route.route.network_area_id - routing_table_id = stackit_routing_table_route.route.routing_table_id - } - `, - testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table route - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "routing_table_id", - "data.stackit_routing_table_route.route", "routing_table_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "route_id", - "data.stackit_routing_table_route.route", "route_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "region", testutil.Region), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "labels.%", "0"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "updated_at"), - - // Routing table routes - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "region", testutil.Region), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "routing_table_id", - "data.stackit_routing_table_routes.routes", "routing_table_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "route_id", - "data.stackit_routing_table_routes.routes", "routes.0.route_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.labels.%", "0"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigRoutingTableRouteMinUpdated, - ResourceName: "stackit_routing_table_route.route", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_routing_table_route.route"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_routing_table_route.route") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - routingTableId, ok := r.Primary.Attributes["routing_table_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute routing_table_id") - } - routeId, ok := r.Primary.Attributes["route_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute route_id") - } - return fmt.Sprintf("%s,%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId, routeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigRoutingTableRouteMinUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMinConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["routing_table_name"])), - - // Routing table route - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "stackit_routing_table_route.route", "routing_table_id", - ), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["destination_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["destination_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["next_hop_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["next_hop_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "0"), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) - }) - - t.Run("TestAccRoutingTableRouteMax", func(t *testing.T) { - t.Logf("TestAccRoutingTableRouteMax") - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigRoutingTableRouteMax, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["routing_table_name"])), - - // Routing table route - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "stackit_routing_table_route.route", "routing_table_id", - ), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["label"])), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), - ), - }, - // Data sources - { - ConfigVariables: testConfigRoutingTableRouteMax, - Config: fmt.Sprintf(` - %s - %s - - # single routing table route - data "stackit_routing_table_route" "route" { - organization_id = stackit_routing_table_route.route.organization_id - network_area_id = stackit_routing_table_route.route.network_area_id - routing_table_id = stackit_routing_table_route.route.routing_table_id - route_id = stackit_routing_table_route.route.route_id - } - - # all routing table routes in routing table - data "stackit_routing_table_routes" "routes" { - organization_id = stackit_routing_table_route.route.organization_id - network_area_id = stackit_routing_table_route.route.network_area_id - routing_table_id = stackit_routing_table_route.route.routing_table_id - } - `, - testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table route - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "routing_table_id", - "data.stackit_routing_table_route.route", "routing_table_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "route_id", - "data.stackit_routing_table_route.route", "route_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "region", testutil.Region), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["label"])), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "updated_at"), - - // Routing table routes - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "region", testutil.Region), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.#", "1"), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "routing_table_id", - "data.stackit_routing_table_routes.routes", "routing_table_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_routing_table_route.route", "route_id", - "data.stackit_routing_table_routes.routes", "routes.0.route_id", - ), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_type"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_value"])), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["label"])), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.created_at"), - resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigRoutingTableRouteMaxUpdated, - ResourceName: "stackit_routing_table_route.route", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_routing_table_route.route"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_routing_table_route.route") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - networkAreaId, ok := r.Primary.Attributes["network_area_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute network_area_id") - } - routingTableId, ok := r.Primary.Attributes["routing_table_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute routing_table_id") - } - routeId, ok := r.Primary.Attributes["route_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute route_id") - } - return fmt.Sprintf("%s,%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId, routeId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigRoutingTableRouteMaxUpdated, - Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfigWithExperiments(), resourceRoutingTableRouteMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Routing table - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), - resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["routing_table_name"])), - - // Routing table route - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["organization_id"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["network_area_id"])), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), - resource.TestCheckResourceAttrPair( - "stackit_routing_table.routing_table", "routing_table_id", - "stackit_routing_table_route.route", "routing_table_id", - ), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["destination_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["destination_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["next_hop_type"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["next_hop_value"])), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["label"])), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), - resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) - }) -} - -func testAccCheckDestroy(s *terraform.State) error { - checkFunctions := []func(s *terraform.State) error{ - testAccCheckRoutingTableDestroy, - testAccCheckRoutingTableRouteDestroy, - } - var errs []error - - wg := sync.WaitGroup{} - wg.Add(len(checkFunctions)) - - for _, f := range checkFunctions { - go func() { - err := f(s) - if err != nil { - errs = append(errs, err) - } - wg.Done() - }() - } - wg.Wait() - return errors.Join(errs...) -} - -func testAccCheckRoutingTableDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaasalpha.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaasalpha.NewAPIClient() - } else { - client, err = iaasalpha.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - // routing tables - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_routing_table" { - continue - } - routingTableId := strings.Split(rs.Primary.ID, core.Separator)[3] - region := strings.Split(rs.Primary.ID, core.Separator)[1] - err := client.DeleteRoutingTableFromAreaExecute(ctx, testutil.OrganizationId, testNetworkAreaId, region, routingTableId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger routing table deletion %q: %w", routingTableId, err)) - } - } - - return errors.Join(errs...) -} - -func testAccCheckRoutingTableRouteDestroy(s *terraform.State) error { - ctx := context.Background() - var client *iaasalpha.APIClient - var err error - if testutil.IaaSCustomEndpoint == "" { - client, err = iaasalpha.NewAPIClient() - } else { - client, err = iaasalpha.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - // routes - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_routing_table_route" { - continue - } - routingTableRouteId := strings.Split(rs.Primary.ID, core.Separator)[4] - routingTableId := strings.Split(rs.Primary.ID, core.Separator)[3] - region := strings.Split(rs.Primary.ID, core.Separator)[1] - err := client.DeleteRouteFromRoutingTableExecute(ctx, testutil.OrganizationId, testNetworkAreaId, region, routingTableId, routingTableRouteId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger routing table route deletion %q: %w", routingTableId, err)) - } - } - - return errors.Join(errs...) -} diff --git a/stackit/internal/services/iaasalpha/routingtable/route/datasource.go b/stackit/internal/services/iaasalpha/routingtable/route/datasource.go deleted file mode 100644 index bd978c07..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/route/datasource.go +++ /dev/null @@ -1,124 +0,0 @@ -package route - -import ( - "context" - "fmt" - "net/http" - - shared "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &routingTableRouteDataSource{} -) - -// NewRoutingTableRouteDataSource is a helper function to simplify the provider implementation. -func NewRoutingTableRouteDataSource() datasource.DataSource { - return &routingTableRouteDataSource{} -} - -// routingTableRouteDataSource is the data source implementation. -type routingTableRouteDataSource struct { - client *iaasalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *routingTableRouteDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_routing_table_route" -} - -func (d *routingTableRouteDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table_route", core.Datasource, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *routingTableRouteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Routing table route datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), - Attributes: shared.GetRouteDataSourceAttributes(), - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *routingTableRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model shared.RouteModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - routeId := model.RouteId.ValueString() - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "route_id", routeId) - - routeResp, err := d.client.GetRouteOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, err.Error(), err.Error()) - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading routing table route", - fmt.Sprintf("Routing table route with ID %q, routing table with ID %q or network area with ID %q does not exist in organization %q.", routeId, routingTableId, networkAreaId, organizationId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = shared.MapRouteModel(ctx, routeResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table route", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table route read") -} diff --git a/stackit/internal/services/iaasalpha/routingtable/route/resource.go b/stackit/internal/services/iaasalpha/routingtable/route/resource.go deleted file mode 100644 index 4ca10104..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/route/resource.go +++ /dev/null @@ -1,570 +0,0 @@ -package route - -import ( - "context" - "fmt" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &routeResource{} - _ resource.ResourceWithConfigure = &routeResource{} - _ resource.ResourceWithImportState = &routeResource{} - _ resource.ResourceWithModifyPlan = &routeResource{} -) - -// NewRoutingTableRouteResource is a helper function to simplify the provider implementation. -func NewRoutingTableRouteResource() resource.Resource { - return &routeResource{} -} - -// routeResource is the resource implementation. -type routeResource struct { - client *iaasalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *routeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_routing_table_route" -} - -// Configure adds the provider configured client to the resource. -func (r *routeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckExperimentEnabled(ctx, &r.providerData, features.RoutingTablesExperiment, "stackit_routing_table_route", core.Resource, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - apiClient := iaasalphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "IaaS alpha client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *routeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - - var configModel shared.RouteModel - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - var planModel shared.RouteModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Schema defines the schema for the resource. -func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Routing table route resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Resource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the routing table is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "routing_table_id": schema.StringAttribute{ - Description: "The routing tables ID.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "route_id": schema.StringAttribute{ - Description: "The ID of the route.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "destination": schema.SingleNestedAttribute{ - Description: "Destination of the route.", - Required: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: fmt.Sprintf("CIDRV type. %s %s", utils.FormatPossibleValues("cidrv4", "cidrv6"), "Only `cidrv4` is supported during experimental stage."), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "value": schema.StringAttribute{ - Description: "An CIDR string.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.CIDR(), - }, - }, - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID to which the routing table is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - "next_hop": schema.SingleNestedAttribute{ - Description: "Next hop destination.", - Required: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: "Type of the next hop. " + utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "value": schema.StringAttribute{ - Description: "Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported during experimental stage.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.IP(false), - }, - }, - }, - }, - "created_at": schema.StringAttribute{ - Description: "Date-time when the route was created.", - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: "Date-time when the route was updated.", - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *routeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model shared.RouteModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - - // Create new routing table route - payload, err := toCreatePayload(ctx, &model.RouteReadModel) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table route", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - routeResp, err := r.client.AddRoutesToRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId).AddRoutesToRoutingTablePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table route", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFieldsFromList(ctx, routeResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table route", fmt.Sprintf("Processing API payload: %v", err)) - return - } - ctx = tflog.SetField(ctx, "route_id", model.RouteId.ValueString()) - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table route created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *routeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model shared.RouteModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - routeId := model.RouteId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "route_id", routeId) - - routeResp, err := r.client.GetRouteOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table route", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = shared.MapRouteModel(ctx, routeResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table route", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table route read.") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *routeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model shared.RouteModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - routeId := model.RouteId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "route_id", routeId) - - // Retrieve values from state - var stateModel shared.RouteModel - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table route", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - route, err := r.client.UpdateRouteOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).UpdateRouteOfRoutingTablePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table route", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = shared.MapRouteModel(ctx, route, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table route", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table route updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *routeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model shared.RouteModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - routeId := model.RouteId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "route_id", routeId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing routing table route - err := r.client.DeleteRouteFromRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error routing table route", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Routing table route deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the routing table route resource import identifier is: organization_id,region,network_area_id,routing_table_id,route_id -func (r *routeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 5 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" || idParts[4] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing routing table", - fmt.Sprintf("Expected import identifier with format: [organization_id],[region],[network_area_id],[routing_table_id],[route_id] Got: %q", req.ID), - ) - return - } - - organizationId := idParts[0] - region := idParts[1] - networkAreaId := idParts[2] - routingTableId := idParts[3] - routeId := idParts[4] - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "route_id", routeId) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_id"), networkAreaId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("routing_table_id"), routingTableId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("route_id"), routeId)...) - tflog.Info(ctx, "Routing table route state imported") -} - -func mapFieldsFromList(ctx context.Context, routeResp *iaasalpha.RouteListResponse, model *shared.RouteModel, region string) error { - if routeResp == nil || routeResp.Items == nil { - return fmt.Errorf("response input is nil") - } else if len(*routeResp.Items) < 1 { - return fmt.Errorf("no routes found in response") - } else if len(*routeResp.Items) > 1 { - return fmt.Errorf("more than 1 route found in response") - } - - route := (*routeResp.Items)[0] - return shared.MapRouteModel(ctx, &route, model, region) -} - -func toCreatePayload(ctx context.Context, model *shared.RouteReadModel) (*iaasalpha.AddRoutesToRoutingTablePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - nextHopPayload, err := toNextHopPayload(ctx, model) - if err != nil { - return nil, err - } - destinationPayload, err := toDestinationPayload(ctx, model) - if err != nil { - return nil, err - } - - return &iaasalpha.AddRoutesToRoutingTablePayload{ - Items: &[]iaasalpha.Route{ - { - Labels: &labels, - Nexthop: nextHopPayload, - Destination: destinationPayload, - }, - }, - }, nil -} - -func toUpdatePayload(ctx context.Context, model *shared.RouteModel, currentLabels types.Map) (*iaasalpha.UpdateRouteOfRoutingTablePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaasalpha.UpdateRouteOfRoutingTablePayload{ - Labels: &labels, - }, nil -} - -func toNextHopPayload(ctx context.Context, model *shared.RouteReadModel) (*iaasalpha.RouteNexthop, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if utils.IsUndefined(model.NextHop) { - return nil, nil - } - - nexthopModel := shared.RouteNextHop{} - diags := model.NextHop.As(ctx, &nexthopModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - switch nexthopModel.Type.ValueString() { - case "blackhole": - return sdkUtils.Ptr(iaasalpha.NexthopBlackholeAsRouteNexthop(iaasalpha.NewNexthopBlackhole("blackhole"))), nil - case "internet": - return sdkUtils.Ptr(iaasalpha.NexthopInternetAsRouteNexthop(iaasalpha.NewNexthopInternet("internet"))), nil - case "ipv4": - return sdkUtils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop(iaasalpha.NewNexthopIPv4("ipv4", nexthopModel.Value.ValueString()))), nil - case "ipv6": - return sdkUtils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop(iaasalpha.NewNexthopIPv6("ipv6", nexthopModel.Value.ValueString()))), nil - } - return nil, fmt.Errorf("unknown nexthop type: %s", nexthopModel.Type.ValueString()) -} - -func toDestinationPayload(ctx context.Context, model *shared.RouteReadModel) (*iaasalpha.RouteDestination, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if utils.IsUndefined(model.Destination) { - return nil, nil - } - - destinationModel := shared.RouteDestination{} - diags := model.Destination.As(ctx, &destinationModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - switch destinationModel.Type.ValueString() { - case "cidrv4": - return sdkUtils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination(iaasalpha.NewDestinationCIDRv4("cidrv4", destinationModel.Value.ValueString()))), nil - case "cidrv6": - return sdkUtils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination(iaasalpha.NewDestinationCIDRv6("cidrv6", destinationModel.Value.ValueString()))), nil - } - return nil, fmt.Errorf("unknown destination type: %s", destinationModel.Type.ValueString()) -} diff --git a/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go b/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go deleted file mode 100644 index 9d59f855..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go +++ /dev/null @@ -1,452 +0,0 @@ -package route - -import ( - "context" - "fmt" - "reflect" - "testing" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" -) - -const ( - testRegion = "eu02" -) - -var ( - organizationId = uuid.New() - networkAreaId = uuid.New() - routingTableId = uuid.New() - routeId = uuid.New() -) - -func Test_mapFieldsFromList(t *testing.T) { - type args struct { - routeResp *iaasalpha.RouteListResponse - model *shared.RouteModel - region string - } - tests := []struct { - name string - args args - wantErr bool - expectedModel *shared.RouteModel - }{ - { - name: "response is nil", - args: args{ - model: &shared.RouteModel{}, - routeResp: nil, - }, - wantErr: true, - }, - { - name: "response items is nil", - args: args{ - model: &shared.RouteModel{}, - routeResp: &iaasalpha.RouteListResponse{ - Items: nil, - }, - }, - wantErr: true, - }, - { - name: "model is nil", - args: args{ - model: nil, - routeResp: &iaasalpha.RouteListResponse{ - Items: nil, - }, - }, - wantErr: true, - }, - { - name: "response items is empty", - args: args{ - model: &shared.RouteModel{}, - routeResp: &iaasalpha.RouteListResponse{ - Items: &[]iaasalpha.Route{}, - }, - }, - wantErr: true, - }, - { - name: "response items contains more than one route", - args: args{ - model: &shared.RouteModel{}, - routeResp: &iaasalpha.RouteListResponse{ - Items: &[]iaasalpha.Route{ - { - Id: utils.Ptr(uuid.NewString()), - }, - { - Id: utils.Ptr(uuid.NewString()), - }, - }, - }, - }, - wantErr: true, - }, - { - name: "success", - args: args{ - model: &shared.RouteModel{ - RouteReadModel: shared.RouteReadModel{ - RouteId: types.StringNull(), - }, - RoutingTableId: types.StringValue(routingTableId.String()), - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - }, - routeResp: &iaasalpha.RouteListResponse{ - Items: &[]iaasalpha.Route{ - { - Id: utils.Ptr(routeId.String()), - Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( - iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), - )), - Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( - iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), - )), - Labels: &map[string]interface{}{ - "foo": "bar", - }, - CreatedAt: nil, - UpdatedAt: nil, - }, - }, - }, - region: testRegion, - }, - wantErr: false, - expectedModel: &shared.RouteModel{ - RouteReadModel: shared.RouteReadModel{ - RouteId: types.StringValue(routeId.String()), - NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv4"), - "value": types.StringValue("10.20.42.2"), - }), - Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv4"), - "value": types.StringValue("58.251.236.138/32"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo": types.StringValue("bar"), - }), - CreatedAt: types.StringNull(), - UpdatedAt: types.StringNull(), - }, - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s,%s", organizationId.String(), testRegion, networkAreaId.String(), routingTableId.String(), routeId.String())), - RoutingTableId: types.StringValue(routingTableId.String()), - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - Region: types.StringValue(testRegion), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - if err := mapFieldsFromList(ctx, tt.args.routeResp, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { - t.Errorf("mapFieldsFromList() error = %v, wantErr %v", err, tt.wantErr) - return - } - - diff := cmp.Diff(tt.args.model, tt.expectedModel) - if diff != "" && !tt.wantErr { - t.Fatalf("mapFieldsFromList(): %s", diff) - } - }) - } -} - -func Test_toUpdatePayload(t *testing.T) { - type args struct { - model *shared.RouteModel - currentLabels types.Map - } - tests := []struct { - name string - args args - want *iaasalpha.UpdateRouteOfRoutingTablePayload - wantErr bool - }{ - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - { - name: "max", - args: args{ - model: &shared.RouteModel{ - RouteReadModel: shared.RouteReadModel{ - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo1": types.StringValue("bar1"), - "foo2": types.StringValue("bar2"), - }), - }, - }, - currentLabels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo1": types.StringValue("foobar"), - "foo3": types.StringValue("bar3"), - }), - }, - want: &iaasalpha.UpdateRouteOfRoutingTablePayload{ - Labels: &map[string]interface{}{ - "foo1": "bar1", - "foo2": "bar2", - "foo3": nil, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toUpdatePayload(ctx, tt.args.model, tt.args.currentLabels) - if (err != nil) != tt.wantErr { - t.Errorf("toUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - diff := cmp.Diff(got, tt.want) - if diff != "" { - t.Fatalf("toUpdatePayload(): %s", diff) - } - }) - } -} - -func Test_toNextHopPayload(t *testing.T) { - type args struct { - model *shared.RouteReadModel - } - tests := []struct { - name string - args args - want *iaasalpha.RouteNexthop - wantErr bool - }{ - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - { - name: "ipv4", - args: args{ - model: &shared.RouteReadModel{ - NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv4"), - "value": types.StringValue("10.20.42.2"), - }), - }, - }, - wantErr: false, - want: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( - iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), - )), - }, - { - name: "ipv6", - args: args{ - model: &shared.RouteReadModel{ - NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv6"), - "value": types.StringValue("172b:f881:46fe:d89a:9332:90f7:3485:236d"), - }), - }, - }, - wantErr: false, - want: utils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop( - iaasalpha.NewNexthopIPv6("ipv6", "172b:f881:46fe:d89a:9332:90f7:3485:236d"), - )), - }, - { - name: "internet", - args: args{ - model: &shared.RouteReadModel{ - NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("internet"), - "value": types.StringNull(), - }), - }, - }, - wantErr: false, - want: utils.Ptr(iaasalpha.NexthopInternetAsRouteNexthop( - iaasalpha.NewNexthopInternet("internet"), - )), - }, - { - name: "blackhole", - args: args{ - model: &shared.RouteReadModel{ - NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("blackhole"), - "value": types.StringNull(), - }), - }, - }, - wantErr: false, - want: utils.Ptr(iaasalpha.NexthopBlackholeAsRouteNexthop( - iaasalpha.NewNexthopBlackhole("blackhole"), - )), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toNextHopPayload(ctx, tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toNextHopPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("toNextHopPayload() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_toDestinationPayload(t *testing.T) { - type args struct { - model *shared.RouteReadModel - } - tests := []struct { - name string - args args - want *iaasalpha.RouteDestination - wantErr bool - }{ - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - { - name: "cidrv4", - args: args{ - model: &shared.RouteReadModel{ - Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv4"), - "value": types.StringValue("58.251.236.138/32"), - }), - }, - }, - wantErr: false, - want: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( - iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), - )), - }, - { - name: "cidrv6", - args: args{ - model: &shared.RouteReadModel{ - Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv6"), - "value": types.StringValue("2001:0db8:3c4d:1a2b::/64"), - }), - }, - }, - wantErr: false, - want: utils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination( - iaasalpha.NewDestinationCIDRv6("cidrv6", "2001:0db8:3c4d:1a2b::/64"), - )), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toDestinationPayload(ctx, tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toDestinationPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("toDestinationPayload() got = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_toCreatePayload(t *testing.T) { - type args struct { - model *shared.RouteReadModel - } - tests := []struct { - name string - args args - want *iaasalpha.AddRoutesToRoutingTablePayload - wantErr bool - }{ - { - name: "model is nil", - args: args{ - model: nil, - }, - wantErr: true, - }, - { - name: "max", - args: args{ - model: &shared.RouteReadModel{ - NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv4"), - "value": types.StringValue("10.20.42.2"), - }), - Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv4"), - "value": types.StringValue("58.251.236.138/32"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo1": types.StringValue("bar1"), - "foo2": types.StringValue("bar2"), - }), - }, - }, - want: &iaasalpha.AddRoutesToRoutingTablePayload{ - Items: &[]iaasalpha.Route{ - { - Labels: &map[string]interface{}{ - "foo1": "bar1", - "foo2": "bar2", - }, - Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( - iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), - )), - Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( - iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), - )), - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toCreatePayload(ctx, tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toCreatePayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - diff := cmp.Diff(got, tt.want) - if diff != "" { - t.Fatalf("toCreatePayload(): %s", diff) - } - }) - } -} diff --git a/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go b/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go deleted file mode 100644 index dd3e34f0..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go +++ /dev/null @@ -1,187 +0,0 @@ -package routes - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - shared "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &routingTableRoutesDataSource{} -) - -type RoutingTableRoutesDataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - RoutingTableId types.String `tfsdk:"routing_table_id"` - Region types.String `tfsdk:"region"` - Routes types.List `tfsdk:"routes"` -} - -// NewRoutingTableRoutesDataSource is a helper function to simplify the provider implementation. -func NewRoutingTableRoutesDataSource() datasource.DataSource { - return &routingTableRoutesDataSource{} -} - -// routingTableDataSource is the data source implementation. -type routingTableRoutesDataSource struct { - client *iaasalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *routingTableRoutesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_routing_table_routes" -} - -func (d *routingTableRoutesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table_routes", core.Datasource, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *routingTableRoutesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Routing table routes datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), - Attributes: shared.GetRoutesDataSourceAttributes(), - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *routingTableRoutesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model RoutingTableRoutesDataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - networkAreaId := model.NetworkAreaId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - - routesResp, err := d.client.ListRoutesOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading routes of routing table", - fmt.Sprintf("Routing table with ID %q in network area with ID %q does not exist in organization %q.", routingTableId, networkAreaId, organizationId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapDataSourceRoutingTableRoutes(ctx, routesResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table routes", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table routes read") -} - -func mapDataSourceRoutingTableRoutes(ctx context.Context, routes *iaasalpha.RouteListResponse, model *RoutingTableRoutesDataSourceModel, region string) error { - if routes == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - if routes.Items == nil { - return fmt.Errorf("items input is nil") - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - - idParts := []string{organizationId, region, networkAreaId, routingTableId} - model.Id = types.StringValue( - strings.Join(idParts, core.Separator), - ) - - itemsList := []attr.Value{} - for i, route := range *routes.Items { - var routeModel shared.RouteReadModel - err := shared.MapRouteReadModel(ctx, &route, &routeModel) - if err != nil { - return fmt.Errorf("mapping route: %w", err) - } - - routeMap := map[string]attr.Value{ - "route_id": routeModel.RouteId, - "destination": routeModel.Destination, - "next_hop": routeModel.NextHop, - "labels": routeModel.Labels, - "created_at": routeModel.CreatedAt, - "updated_at": routeModel.UpdatedAt, - } - - routeTF, diags := types.ObjectValue(shared.RouteReadModelTypes(), routeMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - itemsList = append(itemsList, routeTF) - } - - routesListTF, diags := types.ListValue(types.ObjectType{AttrTypes: shared.RouteReadModelTypes()}, itemsList) - if diags.HasError() { - return core.DiagsToError(diags) - } - - model.Region = types.StringValue(region) - model.Routes = routesListTF - - return nil -} diff --git a/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go deleted file mode 100644 index 171abd65..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package routes - -import ( - "context" - "fmt" - "testing" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" -) - -const ( - testRegion = "eu02" -) - -var ( - testOrganizationId = uuid.NewString() - testNetworkAreaId = uuid.NewString() - testRoutingTableId = uuid.NewString() - testRouteId1 = uuid.NewString() - testRouteId2 = uuid.NewString() -) - -func Test_mapDataSourceRoutingTableRoutes(t *testing.T) { - type args struct { - routes *iaasalpha.RouteListResponse - model *RoutingTableRoutesDataSourceModel - region string - } - tests := []struct { - name string - args args - wantErr bool - expectedModel *RoutingTableRoutesDataSourceModel - }{ - { - name: "model is nil", - args: args{ - model: nil, - routes: &iaasalpha.RouteListResponse{ - Items: &[]iaasalpha.Route{}, - }, - }, - wantErr: true, - }, - { - name: "response is nil", - args: args{ - model: &RoutingTableRoutesDataSourceModel{}, - routes: nil, - }, - wantErr: true, - }, - { - name: "response items is nil", - args: args{ - model: nil, - routes: &iaasalpha.RouteListResponse{ - Items: nil, - }, - }, - wantErr: true, - }, - { - name: "response items is empty", - args: args{ - model: &RoutingTableRoutesDataSourceModel{ - OrganizationId: types.StringValue(testOrganizationId), - NetworkAreaId: types.StringValue(testNetworkAreaId), - RoutingTableId: types.StringValue(testRoutingTableId), - Region: types.StringValue(testRegion), - }, - routes: &iaasalpha.RouteListResponse{ - Items: &[]iaasalpha.Route{}, - }, - region: testRegion, - }, - wantErr: false, - expectedModel: &RoutingTableRoutesDataSourceModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testOrganizationId, testRegion, testNetworkAreaId, testRoutingTableId)), - OrganizationId: types.StringValue(testOrganizationId), - NetworkAreaId: types.StringValue(testNetworkAreaId), - RoutingTableId: types.StringValue(testRoutingTableId), - Region: types.StringValue(testRegion), - Routes: types.ListValueMust( - types.ObjectType{AttrTypes: shared.RouteReadModelTypes()}, []attr.Value{}, - ), - }, - }, - { - name: "response items has items", - args: args{ - model: &RoutingTableRoutesDataSourceModel{ - OrganizationId: types.StringValue(testOrganizationId), - NetworkAreaId: types.StringValue(testNetworkAreaId), - RoutingTableId: types.StringValue(testRoutingTableId), - Region: types.StringValue(testRegion), - }, - routes: &iaasalpha.RouteListResponse{ - Items: &[]iaasalpha.Route{ - { - Id: utils.Ptr(testRouteId1), - Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( - iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), - )), - Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( - iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), - )), - Labels: &map[string]interface{}{ - "foo": "bar", - }, - CreatedAt: nil, - UpdatedAt: nil, - }, - { - Id: utils.Ptr(testRouteId2), - Destination: utils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination( - iaasalpha.NewDestinationCIDRv6("cidrv6", "2001:0db8:3c4d:1a2b::/64"), - )), - Nexthop: utils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop( - iaasalpha.NewNexthopIPv6("ipv6", "172b:f881:46fe:d89a:9332:90f7:3485:236d"), - )), - Labels: &map[string]interface{}{ - "key": "value", - }, - CreatedAt: nil, - UpdatedAt: nil, - }, - }, - }, - region: testRegion, - }, - wantErr: false, - expectedModel: &RoutingTableRoutesDataSourceModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testOrganizationId, testRegion, testNetworkAreaId, testRoutingTableId)), - OrganizationId: types.StringValue(testOrganizationId), - NetworkAreaId: types.StringValue(testNetworkAreaId), - RoutingTableId: types.StringValue(testRoutingTableId), - Region: types.StringValue(testRegion), - Routes: types.ListValueMust( - types.ObjectType{AttrTypes: shared.RouteReadModelTypes()}, []attr.Value{ - types.ObjectValueMust(shared.RouteReadModelTypes(), map[string]attr.Value{ - "route_id": types.StringValue(testRouteId1), - "created_at": types.StringNull(), - "updated_at": types.StringNull(), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo": types.StringValue("bar"), - }), - "destination": types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv4"), - "value": types.StringValue("58.251.236.138/32"), - }), - "next_hop": types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv4"), - "value": types.StringValue("10.20.42.2"), - }), - }), - types.ObjectValueMust(shared.RouteReadModelTypes(), map[string]attr.Value{ - "route_id": types.StringValue(testRouteId2), - "created_at": types.StringNull(), - "updated_at": types.StringNull(), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - "destination": types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv6"), - "value": types.StringValue("2001:0db8:3c4d:1a2b::/64"), - }), - "next_hop": types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv6"), - "value": types.StringValue("172b:f881:46fe:d89a:9332:90f7:3485:236d"), - }), - }), - }, - ), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - if err := mapDataSourceRoutingTableRoutes(ctx, tt.args.routes, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { - t.Errorf("mapDataSourceRoutingTableRoutes() error = %v, wantErr %v", err, tt.wantErr) - return - } - - diff := cmp.Diff(tt.args.model, tt.expectedModel) - if diff != "" && !tt.wantErr { - t.Fatalf("mapFieldsFromList(): %s", diff) - } - }) - } -} diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/route.go b/stackit/internal/services/iaasalpha/routingtable/shared/route.go deleted file mode 100644 index e05cf78d..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/shared/route.go +++ /dev/null @@ -1,213 +0,0 @@ -package shared - -import ( - "context" - "fmt" - "strings" - "time" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" -) - -type RouteReadModel struct { - RouteId types.String `tfsdk:"route_id"` - Destination types.Object `tfsdk:"destination"` - NextHop types.Object `tfsdk:"next_hop"` - Labels types.Map `tfsdk:"labels"` - CreatedAt types.String `tfsdk:"created_at"` - UpdatedAt types.String `tfsdk:"updated_at"` -} - -func RouteReadModelTypes() map[string]attr.Type { - return map[string]attr.Type{ - "route_id": types.StringType, - "destination": types.ObjectType{AttrTypes: RouteDestinationTypes}, - "next_hop": types.ObjectType{AttrTypes: RouteNextHopTypes}, - "labels": types.MapType{ElemType: types.StringType}, - "created_at": types.StringType, - "updated_at": types.StringType, - } -} - -type RouteModel struct { - RouteReadModel - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - RoutingTableId types.String `tfsdk:"routing_table_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Region types.String `tfsdk:"region"` -} - -func RouteModelTypes() map[string]attr.Type { - modelTypes := RouteReadModelTypes() - modelTypes["id"] = types.StringType - modelTypes["organization_id"] = types.StringType - modelTypes["routing_table_id"] = types.StringType - modelTypes["network_area_id"] = types.StringType - modelTypes["region"] = types.StringType - return modelTypes -} - -// RouteDestination is the struct corresponding to RouteReadModel.Destination -type RouteDestination struct { - Type types.String `tfsdk:"type"` - Value types.String `tfsdk:"value"` -} - -// RouteDestinationTypes Types corresponding to routeDestination -var RouteDestinationTypes = map[string]attr.Type{ - "type": types.StringType, - "value": types.StringType, -} - -// RouteNextHop is the struct corresponding to RouteReadModel.NextHop -type RouteNextHop struct { - Type types.String `tfsdk:"type"` - Value types.String `tfsdk:"value"` -} - -// RouteNextHopTypes Types corresponding to routeNextHop -var RouteNextHopTypes = map[string]attr.Type{ - "type": types.StringType, - "value": types.StringType, -} - -func MapRouteModel(ctx context.Context, route *iaasalpha.Route, model *RouteModel, region string) error { - if route == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - err := MapRouteReadModel(ctx, route, &model.RouteReadModel) - if err != nil { - return err - } - - idParts := []string{ - model.OrganizationId.ValueString(), - region, - model.NetworkAreaId.ValueString(), - model.RoutingTableId.ValueString(), - model.RouteId.ValueString(), - } - model.Id = types.StringValue( - strings.Join(idParts, core.Separator), - ) - model.Region = types.StringValue(region) - - return nil -} - -func MapRouteReadModel(ctx context.Context, route *iaasalpha.Route, model *RouteReadModel) error { - if route == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var routeId string - if model.RouteId.ValueString() != "" { - routeId = model.RouteId.ValueString() - } else if route.Id != nil { - routeId = *route.Id - } else { - return fmt.Errorf("routing table route id not present") - } - - labels, err := iaasUtils.MapLabels(ctx, route.Labels, model.Labels) - if err != nil { - return err - } - - // created at and updated at - createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() - if route.CreatedAt != nil { - createdAtValue := *route.CreatedAt - createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - if route.UpdatedAt != nil { - updatedAtValue := *route.UpdatedAt - updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) - } - - // destination - model.Destination, err = MapRouteDestination(route) - if err != nil { - return fmt.Errorf("error mapping route destination: %w", err) - } - - // next hop - model.NextHop, err = MapRouteNextHop(route) - if err != nil { - return fmt.Errorf("error mapping route next hop: %w", err) - } - - model.RouteId = types.StringValue(routeId) - model.CreatedAt = createdAtTF - model.UpdatedAt = updatedAtTF - model.Labels = labels - return nil -} - -func MapRouteNextHop(routeResp *iaasalpha.Route) (types.Object, error) { - if routeResp.Nexthop == nil { - return types.ObjectNull(RouteNextHopTypes), nil - } - - nextHopMap := map[string]attr.Value{} - switch i := routeResp.Nexthop.GetActualInstance().(type) { - case *iaasalpha.NexthopIPv4: - nextHopMap["type"] = types.StringValue(*i.Type) - nextHopMap["value"] = types.StringPointerValue(i.Value) - case *iaasalpha.NexthopIPv6: - nextHopMap["type"] = types.StringValue(*i.Type) - nextHopMap["value"] = types.StringPointerValue(i.Value) - case *iaasalpha.NexthopBlackhole: - nextHopMap["type"] = types.StringValue(*i.Type) - nextHopMap["value"] = types.StringNull() - case *iaasalpha.NexthopInternet: - nextHopMap["type"] = types.StringValue(*i.Type) - nextHopMap["value"] = types.StringNull() - default: - return types.ObjectNull(RouteNextHopTypes), fmt.Errorf("unexpected Nexthop type: %T", i) - } - - nextHopTF, diags := types.ObjectValue(RouteNextHopTypes, nextHopMap) - if diags.HasError() { - return types.ObjectNull(RouteNextHopTypes), core.DiagsToError(diags) - } - - return nextHopTF, nil -} - -func MapRouteDestination(routeResp *iaasalpha.Route) (types.Object, error) { - if routeResp.Destination == nil { - return types.ObjectNull(RouteDestinationTypes), nil - } - - destinationMap := map[string]attr.Value{} - switch i := routeResp.Destination.GetActualInstance().(type) { - case *iaasalpha.DestinationCIDRv4: - destinationMap["type"] = types.StringValue(*i.Type) - destinationMap["value"] = types.StringPointerValue(i.Value) - case *iaasalpha.DestinationCIDRv6: - destinationMap["type"] = types.StringValue(*i.Type) - destinationMap["value"] = types.StringPointerValue(i.Value) - default: - return types.ObjectNull(RouteDestinationTypes), fmt.Errorf("unexpected Destionation type: %T", i) - } - - destinationTF, diags := types.ObjectValue(RouteDestinationTypes, destinationMap) - if diags.HasError() { - return types.ObjectNull(RouteDestinationTypes), core.DiagsToError(diags) - } - - return destinationTF, nil -} diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go b/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go deleted file mode 100644 index 6997ad22..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go +++ /dev/null @@ -1,310 +0,0 @@ -package shared - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" -) - -const ( - testRegion = "eu02" -) - -var ( - testRouteId = uuid.New() - testOrganizationId = uuid.New() - testNetworkAreaId = uuid.New() - testRoutingTableId = uuid.New() -) - -func Test_MapRouteNextHop(t *testing.T) { - type args struct { - routeResp *iaasalpha.Route - } - tests := []struct { - name string - args args - wantErr bool - expected types.Object - }{ - { - name: "nexthop is nil", - args: args{ - routeResp: &iaasalpha.Route{ - Nexthop: nil, - }, - }, - wantErr: false, - expected: types.ObjectNull(RouteNextHopTypes), - }, - { - name: "nexthop is empty", - args: args{ - routeResp: &iaasalpha.Route{ - Nexthop: &iaasalpha.RouteNexthop{}, - }, - }, - wantErr: true, - }, - { - name: "nexthop ipv4", - args: args{ - routeResp: &iaasalpha.Route{ - Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( - iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), - )), - }, - }, - wantErr: false, - expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv4"), - "value": types.StringValue("10.20.42.2"), - }), - }, - { - name: "nexthop ipv6", - args: args{ - routeResp: &iaasalpha.Route{ - Nexthop: utils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop( - iaasalpha.NewNexthopIPv6("ipv6", "172b:f881:46fe:d89a:9332:90f7:3485:236d"), - )), - }, - }, - wantErr: false, - expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv6"), - "value": types.StringValue("172b:f881:46fe:d89a:9332:90f7:3485:236d"), - }), - }, - { - name: "nexthop internet", - args: args{ - routeResp: &iaasalpha.Route{ - Nexthop: utils.Ptr(iaasalpha.NexthopInternetAsRouteNexthop( - iaasalpha.NewNexthopInternet("internet"), - )), - }, - }, - wantErr: false, - expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("internet"), - "value": types.StringNull(), - }), - }, - { - name: "nexthop blackhole", - args: args{ - routeResp: &iaasalpha.Route{ - Nexthop: utils.Ptr(iaasalpha.NexthopBlackholeAsRouteNexthop( - iaasalpha.NewNexthopBlackhole("blackhole"), - )), - }, - }, - wantErr: false, - expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("blackhole"), - "value": types.StringNull(), - }), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - actual, err := MapRouteNextHop(tt.args.routeResp) - if (err != nil) != tt.wantErr { - t.Errorf("mapNextHop() error = %v, wantErr %v", err, tt.wantErr) - } - - diff := cmp.Diff(actual, tt.expected) - if !tt.wantErr && diff != "" { - t.Errorf("mapNextHop() result does not match: %s", diff) - } - }) - } -} - -func Test_MapRouteDestination(t *testing.T) { - type args struct { - routeResp *iaasalpha.Route - } - tests := []struct { - name string - args args - wantErr bool - expected types.Object - }{ - - { - name: "destination is nil", - args: args{ - routeResp: &iaasalpha.Route{ - Destination: nil, - }, - }, - wantErr: false, - expected: types.ObjectNull(RouteDestinationTypes), - }, - { - name: "destination is empty", - args: args{ - routeResp: &iaasalpha.Route{ - Destination: &iaasalpha.RouteDestination{}, - }, - }, - wantErr: true, - }, - { - name: "destination cidrv4", - args: args{ - routeResp: &iaasalpha.Route{ - Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( - iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), - )), - }, - }, - wantErr: false, - expected: types.ObjectValueMust(RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv4"), - "value": types.StringValue("58.251.236.138/32"), - }), - }, - { - name: "destination cidrv6", - args: args{ - routeResp: &iaasalpha.Route{ - Destination: utils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination( - iaasalpha.NewDestinationCIDRv6("cidrv6", "2001:0db8:3c4d:1a2b::/64"), - )), - }, - }, - wantErr: false, - expected: types.ObjectValueMust(RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv6"), - "value": types.StringValue("2001:0db8:3c4d:1a2b::/64"), - }), - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - actual, err := MapRouteDestination(tt.args.routeResp) - if (err != nil) != tt.wantErr { - t.Errorf("mapDestination() error = %v, wantErr %v", err, tt.wantErr) - } - - diff := cmp.Diff(actual, tt.expected) - if !tt.wantErr && diff != "" { - t.Errorf("mapDestination() result does not match: %s", diff) - } - }) - } -} - -func TestMapRouteModel(t *testing.T) { - createdAt := time.Now() - updatedAt := time.Now().Add(5 * time.Minute) - - type args struct { - route *iaasalpha.Route - model *RouteModel - region string - } - tests := []struct { - name string - args args - wantErr bool - expectedModel *RouteModel - }{ - { - name: "route is nil", - args: args{ - model: &RouteModel{}, - route: nil, - region: testRegion, - }, - wantErr: true, - }, - { - name: "model is nil", - args: args{ - model: nil, - route: &iaasalpha.Route{}, - region: testRegion, - }, - wantErr: true, - }, - { - name: "max", - args: args{ - model: &RouteModel{ - // state - OrganizationId: types.StringValue(testOrganizationId.String()), - NetworkAreaId: types.StringValue(testNetworkAreaId.String()), - RoutingTableId: types.StringValue(testRoutingTableId.String()), - }, - route: &iaasalpha.Route{ - Id: utils.Ptr(testRouteId.String()), - Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( - iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), - )), - Labels: &map[string]interface{}{ - "foo1": "bar1", - "foo2": "bar2", - }, - Nexthop: utils.Ptr( - iaasalpha.NexthopIPv4AsRouteNexthop(iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2")), - ), - CreatedAt: &createdAt, - UpdatedAt: &updatedAt, - }, - region: testRegion, - }, - wantErr: false, - expectedModel: &RouteModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s,%s", - testOrganizationId.String(), testRegion, testNetworkAreaId.String(), testRoutingTableId.String(), testRouteId.String()), - ), - OrganizationId: types.StringValue(testOrganizationId.String()), - NetworkAreaId: types.StringValue(testNetworkAreaId.String()), - RoutingTableId: types.StringValue(testRoutingTableId.String()), - RouteReadModel: RouteReadModel{ - RouteId: types.StringValue(testRouteId.String()), - Destination: types.ObjectValueMust(RouteDestinationTypes, map[string]attr.Value{ - "type": types.StringValue("cidrv4"), - "value": types.StringValue("58.251.236.138/32"), - }), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "foo1": types.StringValue("bar1"), - "foo2": types.StringValue("bar2"), - }), - NextHop: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ - "type": types.StringValue("ipv4"), - "value": types.StringValue("10.20.42.2"), - }), - CreatedAt: types.StringValue(createdAt.Format(time.RFC3339)), - UpdatedAt: types.StringValue(updatedAt.Format(time.RFC3339)), - }, - Region: types.StringValue(testRegion), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - if err := MapRouteModel(ctx, tt.args.route, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { - t.Errorf("MapRouteModel() error = %v, wantErr %v", err, tt.wantErr) - } - - diff := cmp.Diff(tt.args.model, tt.expectedModel) - if !tt.wantErr && diff != "" { - t.Errorf("MapRouteModel() model does not match: %s", diff) - } - }) - } -} diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/shared.go b/stackit/internal/services/iaasalpha/routingtable/shared/shared.go deleted file mode 100644 index 04382ad3..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/shared/shared.go +++ /dev/null @@ -1,264 +0,0 @@ -package shared - -import ( - "context" - "fmt" - "maps" - "time" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -type RoutingTableReadModel struct { - RoutingTableId types.String `tfsdk:"routing_table_id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - Labels types.Map `tfsdk:"labels"` - CreatedAt types.String `tfsdk:"created_at"` - UpdatedAt types.String `tfsdk:"updated_at"` - Default types.Bool `tfsdk:"default"` - SystemRoutes types.Bool `tfsdk:"system_routes"` -} - -func RoutingTableReadModelTypes() map[string]attr.Type { - return map[string]attr.Type{ - "routing_table_id": types.StringType, - "name": types.StringType, - "description": types.StringType, - "labels": types.MapType{ElemType: types.StringType}, - "created_at": types.StringType, - "updated_at": types.StringType, - "default": types.BoolType, - "system_routes": types.BoolType, - } -} - -type RoutingTableDataSourceModel struct { - RoutingTableReadModel - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Region types.String `tfsdk:"region"` -} - -func GetDatasourceGetAttributes() map[string]schema.Attribute { - // combine the schemas - getAttributes := RoutingTableResponseAttributes() - maps.Copy(getAttributes, datasourceGetAttributes()) - getAttributes["id"] = schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`\".", - Computed: true, - } - return getAttributes -} - -func GetRouteDataSourceAttributes() map[string]schema.Attribute { - getAttributes := datasourceGetAttributes() - maps.Copy(getAttributes, RouteResponseAttributes()) - getAttributes["route_id"] = schema.StringAttribute{ - Description: "Route ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - } - getAttributes["id"] = schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`\".", - Computed: true, - } - return getAttributes -} - -func GetRoutesDataSourceAttributes() map[string]schema.Attribute { - getAttributes := datasourceGetAttributes() - getAttributes["id"] = schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`\".", - Computed: true, - } - getAttributes["routes"] = schema.ListNestedAttribute{ - Description: "List of routes.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: RouteResponseAttributes(), - }, - } - getAttributes["region"] = schema.StringAttribute{ - Description: "The datasource region. If not defined, the provider region is used.", - Optional: true, - } - return getAttributes -} - -func datasourceGetAttributes() map[string]schema.Attribute { - return map[string]schema.Attribute{ - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the routing table is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "routing_table_id": schema.StringAttribute{ - Description: "The routing tables ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID to which the routing table is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - Optional: true, - }, - } -} - -func RouteResponseAttributes() map[string]schema.Attribute { - return map[string]schema.Attribute{ - "route_id": schema.StringAttribute{ - Description: "Route ID.", - Computed: true, - }, - "destination": schema.SingleNestedAttribute{ - Description: "Destination of the route.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: fmt.Sprintf("CIDRV type. %s %s", utils.FormatPossibleValues("cidrv4", "cidrv6"), "Only `cidrv4` is supported during experimental stage."), - Computed: true, - }, - "value": schema.StringAttribute{ - Description: "An CIDR string.", - Computed: true, - }, - }, - }, - "next_hop": schema.SingleNestedAttribute{ - Description: "Next hop destination.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: "Type of the next hop. " + utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), - Computed: true, - }, - "value": schema.StringAttribute{ - Description: "Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported during experimental stage.", - Computed: true, - }, - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: "Date-time when the route was created", - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: "Date-time when the route was updated", - Computed: true, - }, - } -} - -func RoutingTableResponseAttributes() map[string]schema.Attribute { - return map[string]schema.Attribute{ - "routing_table_id": schema.StringAttribute{ - Description: "The routing tables ID.", - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "The name of the routing table.", - Computed: true, - }, - "description": schema.StringAttribute{ - Description: "Description of the routing table.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Computed: true, - }, - "default": schema.BoolAttribute{ - Description: "When true this is the default routing table for this network area. It can't be deleted and is used if the user does not specify it otherwise.", - Computed: true, - }, - "system_routes": schema.BoolAttribute{ - Description: "This controls whether the routes for project-to-project communication are created automatically or not.", - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: "Date-time when the routing table was created", - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: "Date-time when the routing table was updated", - Computed: true, - }, - } -} - -func MapRoutingTableReadModel(ctx context.Context, routingTable *iaasalpha.RoutingTable, model *RoutingTableReadModel) error { - if routingTable == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var routingTableId string - if model.RoutingTableId.ValueString() != "" { - routingTableId = model.RoutingTableId.ValueString() - } else if routingTable.Id != nil { - routingTableId = *routingTable.Id - } else { - return fmt.Errorf("routing table id not present") - } - - labels, err := iaasUtils.MapLabels(ctx, routingTable.Labels, model.Labels) - if err != nil { - return err - } - - // created at and updated at - createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() - if routingTable.CreatedAt != nil { - createdAtValue := *routingTable.CreatedAt - createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - if routingTable.UpdatedAt != nil { - updatedAtValue := *routingTable.UpdatedAt - updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) - } - - model.RoutingTableId = types.StringValue(routingTableId) - model.Name = types.StringPointerValue(routingTable.Name) - model.Description = types.StringPointerValue(routingTable.Description) - model.Default = types.BoolPointerValue(routingTable.Default) - model.SystemRoutes = types.BoolPointerValue(routingTable.SystemRoutes) - model.Labels = labels - model.CreatedAt = createdAtTF - model.UpdatedAt = updatedAtTF - return nil -} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/datasource.go b/stackit/internal/services/iaasalpha/routingtable/table/datasource.go deleted file mode 100644 index fbfb7950..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/table/datasource.go +++ /dev/null @@ -1,160 +0,0 @@ -package table - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &routingTableDataSource{} -) - -// NewRoutingTableDataSource is a helper function to simplify the provider implementation. -func NewRoutingTableDataSource() datasource.DataSource { - return &routingTableDataSource{} -} - -// routingTableDataSource is the data source implementation. -type routingTableDataSource struct { - client *iaasalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *routingTableDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_routing_table" -} - -func (d *routingTableDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table", core.Datasource, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *routingTableDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Routing table datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), - Attributes: shared.GetDatasourceGetAttributes(), - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *routingTableDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model shared.RoutingTableDataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - routingTableResp, err := d.client.GetRoutingTableOfArea(ctx, organizationId, networkAreaId, region, routingTableId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading routing table", - fmt.Sprintf("Routing table with ID %q or network area with ID %q does not exist in organization %q.", routingTableId, networkAreaId, organizationId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapDatasourceFields(ctx, routingTableResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table read") -} - -func mapDatasourceFields(ctx context.Context, routingTable *iaasalpha.RoutingTable, model *shared.RoutingTableDataSourceModel, region string) error { - if routingTable == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var routingTableId string - if model.RoutingTableId.ValueString() != "" { - routingTableId = model.RoutingTableId.ValueString() - } else if routingTable.Id != nil { - routingTableId = *routingTable.Id - } else { - return fmt.Errorf("routing table id not present") - } - - idParts := []string{ - model.OrganizationId.ValueString(), - region, - model.NetworkAreaId.ValueString(), - routingTableId, - } - model.Id = types.StringValue( - strings.Join(idParts, core.Separator), - ) - - err := shared.MapRoutingTableReadModel(ctx, routingTable, &model.RoutingTableReadModel) - if err != nil { - return err - } - - model.Region = types.StringValue(region) - return nil -} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go deleted file mode 100644 index 4622e1b3..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package table - -import ( - "context" - "fmt" - "testing" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" -) - -const ( - testRegion = "eu01" -) - -var ( - organizationId = uuid.New() - networkAreaId = uuid.New() - routingTableId = uuid.New() -) - -func Test_mapDatasourceFields(t *testing.T) { - id := fmt.Sprintf("%s,%s,%s,%s", organizationId.String(), testRegion, networkAreaId.String(), routingTableId.String()) - - tests := []struct { - description string - state shared.RoutingTableDataSourceModel - input *iaasalpha.RoutingTable - expected shared.RoutingTableDataSourceModel - isValid bool - }{ - { - "default_values", - shared.RoutingTableDataSourceModel{ - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - }, - &iaasalpha.RoutingTable{ - Id: utils.Ptr(routingTableId.String()), - Name: utils.Ptr("default_values"), - }, - shared.RoutingTableDataSourceModel{ - Id: types.StringValue(id), - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - Region: types.StringValue(testRegion), - RoutingTableReadModel: shared.RoutingTableReadModel{ - RoutingTableId: types.StringValue(routingTableId.String()), - Name: types.StringValue("default_values"), - Labels: types.MapNull(types.StringType), - }, - }, - true, - }, - { - "values_ok", - shared.RoutingTableDataSourceModel{ - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - RoutingTableReadModel: shared.RoutingTableReadModel{}, - }, - &iaasalpha.RoutingTable{ - Id: utils.Ptr(routingTableId.String()), - Name: utils.Ptr("values_ok"), - Description: utils.Ptr("Description"), - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - shared.RoutingTableDataSourceModel{ - Id: types.StringValue(id), - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - Region: types.StringValue(testRegion), - RoutingTableReadModel: shared.RoutingTableReadModel{ - RoutingTableId: types.StringValue(routingTableId.String()), - Name: types.StringValue("values_ok"), - Description: types.StringValue("Description"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - }, - true, - }, - { - "response_fields_nil_fail", - shared.RoutingTableDataSourceModel{}, - &iaasalpha.RoutingTable{ - Id: nil, - }, - shared.RoutingTableDataSourceModel{}, - false, - }, - { - "response_nil_fail", - shared.RoutingTableDataSourceModel{}, - nil, - shared.RoutingTableDataSourceModel{}, - false, - }, - { - "no_resource_id", - shared.RoutingTableDataSourceModel{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - }, - &iaasalpha.RoutingTable{}, - shared.RoutingTableDataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDatasourceFields(context.Background(), tt.input, &tt.state, testRegion) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/resource.go b/stackit/internal/services/iaasalpha/routingtable/table/resource.go deleted file mode 100644 index 4b9fb1b0..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/table/resource.go +++ /dev/null @@ -1,526 +0,0 @@ -package table - -import ( - "context" - "fmt" - "net/http" - "strings" - "time" - - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &routingTableResource{} - _ resource.ResourceWithConfigure = &routingTableResource{} - _ resource.ResourceWithImportState = &routingTableResource{} - _ resource.ResourceWithModifyPlan = &routingTableResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - RoutingTableId types.String `tfsdk:"routing_table_id"` - Name types.String `tfsdk:"name"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Description types.String `tfsdk:"description"` - Labels types.Map `tfsdk:"labels"` - Region types.String `tfsdk:"region"` - SystemRoutes types.Bool `tfsdk:"system_routes"` - CreatedAt types.String `tfsdk:"created_at"` - UpdatedAt types.String `tfsdk:"updated_at"` -} - -// NewRoutingTableResource is a helper function to simplify the provider implementation. -func NewRoutingTableResource() resource.Resource { - return &routingTableResource{} -} - -// routingTableResource is the resource implementation. -type routingTableResource struct { - client *iaasalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *routingTableResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_routing_table" -} - -// Configure adds the provider configured client to the resource. -func (r *routingTableResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckExperimentEnabled(ctx, &r.providerData, features.RoutingTablesExperiment, "stackit_routing_table", core.Resource, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - apiClient := iaasalphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "IaaS alpha client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *routingTableResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - var configModel Model - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (r *routingTableResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := "Routing table resource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Resource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the routing table is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "routing_table_id": schema.StringAttribute{ - Description: "The routing tables ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the routing table.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID to which the routing table is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "description": schema.StringAttribute{ - Description: "Description of the routing table.", - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtMost(127), - }, - }, - "labels": schema.MapAttribute{ - Description: "Labels are key-value string pairs which can be attached to a resource container", - ElementType: types.StringType, - Optional: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "system_routes": schema.BoolAttribute{ - Description: "This controls whether the routes for project-to-project communication are created automatically or not.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - }, - "created_at": schema.StringAttribute{ - Description: "Date-time when the routing table was created", - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: "Date-time when the routing table was updated", - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *routingTableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - routingTable, err := r.client.AddRoutingTableToArea(ctx, organizationId, networkAreaId, region).AddRoutingTableToAreaPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, routingTable, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table.", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *routingTableResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - routingTableResp, err := r.client.GetRoutingTableOfArea(ctx, organizationId, networkAreaId, region, routingTableId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading routing table", - fmt.Sprintf("routing table with ID %q does not exist in organization %q.", routingTableId, organizationId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, routingTableResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *routingTableResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - routingTable, err := r.client.UpdateRoutingTableOfArea(ctx, organizationId, networkAreaId, region, routingTableId).UpdateRoutingTableOfAreaPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, routingTable, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *routingTableResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - routingTableId := model.RoutingTableId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - // Delete existing routing table - err := r.client.DeleteRoutingTableFromArea(ctx, organizationId, networkAreaId, region, routingTableId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting routing table", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Routing table deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: organization_id,region,network_area_id,routing_table_id -func (r *routingTableResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing routing table", - fmt.Sprintf("Expected import identifier with format: [organization_id],[region],[network_area_id],[routing_table_id] Got: %q", req.ID), - ) - return - } - - organizationId := idParts[0] - region := idParts[1] - networkAreaId := idParts[2] - routingTableId := idParts[3] - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_id"), networkAreaId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("routing_table_id"), routingTableId)...) - tflog.Info(ctx, "Routing table state imported") -} - -func mapFields(ctx context.Context, routingTable *iaasalpha.RoutingTable, model *Model, region string) error { - if routingTable == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var routingTableId string - if model.RoutingTableId.ValueString() != "" { - routingTableId = model.RoutingTableId.ValueString() - } else if routingTable.Id != nil { - routingTableId = *routingTable.Id - } else { - return fmt.Errorf("routing table id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), region, model.NetworkAreaId.ValueString(), routingTableId) - - labels, err := iaasUtils.MapLabels(ctx, routingTable.Labels, model.Labels) - if err != nil { - return err - } - - // created at and updated at - createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() - if routingTable.CreatedAt != nil { - createdAtValue := *routingTable.CreatedAt - createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - if routingTable.UpdatedAt != nil { - updatedAtValue := *routingTable.UpdatedAt - updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) - } - - model.RoutingTableId = types.StringValue(routingTableId) - model.Name = types.StringPointerValue(routingTable.Name) - model.Description = types.StringPointerValue(routingTable.Description) - model.Labels = labels - model.Region = types.StringValue(region) - model.SystemRoutes = types.BoolPointerValue(routingTable.SystemRoutes) - model.CreatedAt = createdAtTF - model.UpdatedAt = updatedAtTF - return nil -} - -func toCreatePayload(ctx context.Context, model *Model) (*iaasalpha.AddRoutingTableToAreaPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaasalpha.AddRoutingTableToAreaPayload{ - Description: conversion.StringValueToPointer(model.Description), - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - SystemRoutes: conversion.BoolValueToPointer(model.SystemRoutes), - }, nil -} - -func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaasalpha.UpdateRoutingTableOfAreaPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &iaasalpha.UpdateRoutingTableOfAreaPayload{ - Description: conversion.StringValueToPointer(model.Description), - Name: conversion.StringValueToPointer(model.Name), - Labels: &labels, - }, nil -} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go b/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go deleted file mode 100644 index 24b1fef9..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go +++ /dev/null @@ -1,212 +0,0 @@ -package table - -import ( - "context" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" -) - -func TestMapFields(t *testing.T) { - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s,%s", "oid", testRegion, "aid", "rtid") - tests := []struct { - description string - state Model - input *iaasalpha.RoutingTable - expected Model - isValid bool - }{ - { - "default_values", - Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("aid"), - }, - &iaasalpha.RoutingTable{ - Id: utils.Ptr("rtid"), - Name: utils.Ptr("default_values"), - }, - Model{ - Id: types.StringValue(id), - OrganizationId: types.StringValue("oid"), - RoutingTableId: types.StringValue("rtid"), - Name: types.StringValue("default_values"), - NetworkAreaId: types.StringValue("aid"), - Labels: types.MapNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "values_ok", - Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("aid"), - }, - &iaasalpha.RoutingTable{ - Id: utils.Ptr("rtid"), - Name: utils.Ptr("values_ok"), - Description: utils.Ptr("Description"), - Labels: &map[string]interface{}{ - "key": "value", - }, - }, - Model{ - Id: types.StringValue(id), - OrganizationId: types.StringValue("oid"), - RoutingTableId: types.StringValue("rtid"), - Name: types.StringValue("values_ok"), - Description: types.StringValue("Description"), - NetworkAreaId: types.StringValue("aid"), - Region: types.StringValue(testRegion), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - }, - true, - }, - { - "response_fields_nil_fail", - Model{}, - &iaasalpha.RoutingTable{ - Id: nil, - }, - Model{}, - false, - }, - { - "response_nil_fail", - Model{}, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - OrganizationId: types.StringValue("oid"), - NetworkAreaId: types.StringValue("naid"), - }, - &iaasalpha.RoutingTable{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state, testRegion) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaasalpha.AddRoutingTableToAreaPayload - isValid bool - }{ - { - description: "default_ok", - input: &Model{ - Description: types.StringValue("Description"), - Name: types.StringValue("default_ok"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - SystemRoutes: types.BoolValue(true), - }, - expected: &iaasalpha.AddRoutingTableToAreaPayload{ - Description: utils.Ptr("Description"), - Name: utils.Ptr("default_ok"), - Labels: &map[string]interface{}{ - "key": "value", - }, - SystemRoutes: utils.Ptr(true), - }, - isValid: true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *iaasalpha.UpdateRoutingTableOfAreaPayload - isValid bool - }{ - { - "default_ok", - &Model{ - Description: types.StringValue("Description"), - Name: types.StringValue("default_ok"), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ - "key1": types.StringValue("value1"), - "key2": types.StringValue("value2"), - }), - }, - &iaasalpha.UpdateRoutingTableOfAreaPayload{ - Description: utils.Ptr("Description"), - Name: utils.Ptr("default_ok"), - Labels: &map[string]interface{}{ - "key1": "value1", - "key2": "value2", - }, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go b/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go deleted file mode 100644 index c6ef9268..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go +++ /dev/null @@ -1,216 +0,0 @@ -package tables - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &routingTablesDataSource{} -) - -type DataSourceModelTables struct { - Id types.String `tfsdk:"id"` // needed by TF - OrganizationId types.String `tfsdk:"organization_id"` - NetworkAreaId types.String `tfsdk:"network_area_id"` - Region types.String `tfsdk:"region"` - Items types.List `tfsdk:"items"` -} - -// NewRoutingTablesDataSource is a helper function to simplify the provider implementation. -func NewRoutingTablesDataSource() datasource.DataSource { - return &routingTablesDataSource{} -} - -// routingTableDataSource is the data source implementation. -type routingTablesDataSource struct { - client *iaasalpha.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *routingTablesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_routing_tables" -} - -func (d *routingTablesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_tables", core.Datasource, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "IaaS client configured") -} - -// Schema defines the schema for the data source. -func (d *routingTablesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - description := "Routing table datasource schema. Must have a `region` specified in the provider configuration." - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`\".", - Computed: true, - }, - "organization_id": schema.StringAttribute{ - Description: "STACKIT organization ID to which the routing table is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "network_area_id": schema.StringAttribute{ - Description: "The network area ID to which the routing table is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: "The resource region. If not defined, the provider region is used.", - // the region cannot be found, so it has to be passed - Optional: true, - }, - "items": schema.ListNestedAttribute{ - Description: "List of routing tables.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: shared.RoutingTableResponseAttributes(), - }, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *routingTablesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModelTables - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - organizationId := model.OrganizationId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - networkAreaId := model.NetworkAreaId.ValueString() - ctx = tflog.SetField(ctx, "organization_id", organizationId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) - - routingTablesResp, err := d.client.ListRoutingTablesOfArea(ctx, organizationId, networkAreaId, region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading routing tables", - fmt.Sprintf("Routing tables with network area with ID %q does not exist in organization %q.", networkAreaId, organizationId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapDataSourceRoutingTables(ctx, routingTablesResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Routing table read") -} - -func mapDataSourceRoutingTables(ctx context.Context, routingTables *iaasalpha.RoutingTableListResponse, model *DataSourceModelTables, region string) error { - if routingTables == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - if routingTables.Items == nil { - return fmt.Errorf("items input is nil") - } - - organizationId := model.OrganizationId.ValueString() - networkAreaId := model.NetworkAreaId.ValueString() - - model.Id = utils.BuildInternalTerraformId(organizationId, region, networkAreaId) - - itemsList := []attr.Value{} - for i, routingTable := range *routingTables.Items { - var routingTableModel shared.RoutingTableReadModel - err := shared.MapRoutingTableReadModel(ctx, &routingTable, &routingTableModel) - if err != nil { - return fmt.Errorf("mapping routes: %w", err) - } - - routingTableMap := map[string]attr.Value{ - "routing_table_id": routingTableModel.RoutingTableId, - "name": routingTableModel.Name, - "description": routingTableModel.Description, - "labels": routingTableModel.Labels, - "created_at": routingTableModel.CreatedAt, - "updated_at": routingTableModel.UpdatedAt, - "default": routingTableModel.Default, - "system_routes": routingTableModel.SystemRoutes, - } - - routingTableTF, diags := types.ObjectValue(shared.RoutingTableReadModelTypes(), routingTableMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - itemsList = append(itemsList, routingTableTF) - } - - itemsListTF, diags := types.ListValue(types.ObjectType{AttrTypes: shared.RoutingTableReadModelTypes()}, itemsList) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.Items = itemsListTF - model.Region = types.StringValue(region) - - return nil -} diff --git a/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go deleted file mode 100644 index 2df93e79..00000000 --- a/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package tables - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" -) - -const ( - testRegion = "eu01" -) - -var ( - organizationId = uuid.New() - networkAreaId = uuid.New() - routingTableId = uuid.New() - secondRoutingTableId = uuid.New() -) - -func TestMapDataFields(t *testing.T) { - terraformId := fmt.Sprintf("%s,%s,%s", organizationId.String(), testRegion, networkAreaId.String()) - createdAt := time.Now() - updatedAt := time.Now().Add(5 * time.Minute) - - tests := []struct { - description string - state DataSourceModelTables - input *iaasalpha.RoutingTableListResponse - expected DataSourceModelTables - isValid bool - }{ - { - "default_values", - DataSourceModelTables{ - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - Region: types.StringValue(testRegion), - }, - &iaasalpha.RoutingTableListResponse{ - Items: &[]iaasalpha.RoutingTable{ - { - Id: utils.Ptr(routingTableId.String()), - Name: utils.Ptr("test"), - Description: utils.Ptr("description"), - Default: utils.Ptr(true), - CreatedAt: &createdAt, - UpdatedAt: &updatedAt, - SystemRoutes: utils.Ptr(false), - }, - }, - }, - DataSourceModelTables{ - Id: types.StringValue(terraformId), - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - Region: types.StringValue(testRegion), - Items: types.ListValueMust(types.ObjectType{AttrTypes: shared.RoutingTableReadModelTypes()}, []attr.Value{ - types.ObjectValueMust(shared.RoutingTableReadModelTypes(), map[string]attr.Value{ - "routing_table_id": types.StringValue(routingTableId.String()), - "name": types.StringValue("test"), - "description": types.StringValue("description"), - "default": types.BoolValue(true), - "system_routes": types.BoolValue(false), - "created_at": types.StringValue(createdAt.Format(time.RFC3339)), - "updated_at": types.StringValue(updatedAt.Format(time.RFC3339)), - "labels": types.MapNull(types.StringType), - }), - }), - }, - true, - }, - { - "two routing tables", - DataSourceModelTables{ - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - Region: types.StringValue(testRegion), - }, - &iaasalpha.RoutingTableListResponse{ - Items: &[]iaasalpha.RoutingTable{ - { - Id: utils.Ptr(routingTableId.String()), - Name: utils.Ptr("test"), - Description: utils.Ptr("description"), - Default: utils.Ptr(true), - CreatedAt: &createdAt, - UpdatedAt: &updatedAt, - SystemRoutes: utils.Ptr(false), - }, - { - Id: utils.Ptr(secondRoutingTableId.String()), - Name: utils.Ptr("test2"), - Description: utils.Ptr("description2"), - Default: utils.Ptr(false), - CreatedAt: &createdAt, - UpdatedAt: &updatedAt, - SystemRoutes: utils.Ptr(false), - }, - }, - }, - DataSourceModelTables{ - Id: types.StringValue(terraformId), - OrganizationId: types.StringValue(organizationId.String()), - NetworkAreaId: types.StringValue(networkAreaId.String()), - Region: types.StringValue(testRegion), - Items: types.ListValueMust(types.ObjectType{AttrTypes: shared.RoutingTableReadModelTypes()}, []attr.Value{ - types.ObjectValueMust(shared.RoutingTableReadModelTypes(), map[string]attr.Value{ - "routing_table_id": types.StringValue(routingTableId.String()), - "name": types.StringValue("test"), - "description": types.StringValue("description"), - "default": types.BoolValue(true), - "system_routes": types.BoolValue(false), - "created_at": types.StringValue(createdAt.Format(time.RFC3339)), - "updated_at": types.StringValue(updatedAt.Format(time.RFC3339)), - "labels": types.MapNull(types.StringType), - }), - types.ObjectValueMust(shared.RoutingTableReadModelTypes(), map[string]attr.Value{ - "routing_table_id": types.StringValue(secondRoutingTableId.String()), - "name": types.StringValue("test2"), - "description": types.StringValue("description2"), - "default": types.BoolValue(false), - "system_routes": types.BoolValue(false), - "created_at": types.StringValue(createdAt.Format(time.RFC3339)), - "updated_at": types.StringValue(updatedAt.Format(time.RFC3339)), - "labels": types.MapNull(types.StringType), - }), - }), - }, - true, - }, - { - "response_fields_items_nil_fail", - DataSourceModelTables{}, - &iaasalpha.RoutingTableListResponse{ - Items: nil, - }, - DataSourceModelTables{}, - false, - }, - { - "response_nil_fail", - DataSourceModelTables{}, - nil, - DataSourceModelTables{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapDataSourceRoutingTables(context.Background(), tt.input, &tt.state, testRegion) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-max.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-max.tf deleted file mode 100644 index 74c656d1..00000000 --- a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-max.tf +++ /dev/null @@ -1,19 +0,0 @@ -variable "organization_id" {} -variable "network_area_id" {} -variable "name" {} -variable "description" {} -variable "region" {} -variable "label" {} -variable "system_routes" {} - -resource "stackit_routing_table" "routing_table" { - organization_id = var.organization_id - network_area_id = var.network_area_id - name = var.name - description = var.description - region = var.region - labels = { - "acc-test" : var.label - } - system_routes = var.system_routes -} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-min.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-min.tf deleted file mode 100644 index 26921d7d..00000000 --- a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-min.tf +++ /dev/null @@ -1,9 +0,0 @@ -variable "organization_id" {} -variable "network_area_id" {} -variable "name" {} - -resource "stackit_routing_table" "routing_table" { - organization_id = var.organization_id - network_area_id = var.network_area_id - name = var.name -} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-max.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-max.tf deleted file mode 100644 index da2833c0..00000000 --- a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-max.tf +++ /dev/null @@ -1,31 +0,0 @@ -variable "organization_id" {} -variable "network_area_id" {} -variable "routing_table_name" {} -variable "destination_type" {} -variable "destination_value" {} -variable "next_hop_type" {} -variable "next_hop_value" {} -variable "label" {} - -resource "stackit_routing_table" "routing_table" { - organization_id = var.organization_id - network_area_id = var.network_area_id - name = var.routing_table_name -} - -resource "stackit_routing_table_route" "route" { - organization_id = var.organization_id - network_area_id = var.network_area_id - routing_table_id = stackit_routing_table.routing_table.routing_table_id - destination = { - type = var.destination_type - value = var.destination_value - } - next_hop = { - type = var.next_hop_type - value = var.next_hop_value - } - labels = { - "acc-test" = var.label - } -} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-min.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-min.tf deleted file mode 100644 index 65336be8..00000000 --- a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-min.tf +++ /dev/null @@ -1,27 +0,0 @@ -variable "organization_id" {} -variable "network_area_id" {} -variable "routing_table_name" {} -variable "destination_type" {} -variable "destination_value" {} -variable "next_hop_type" {} -variable "next_hop_value" {} - -resource "stackit_routing_table" "routing_table" { - organization_id = var.organization_id - network_area_id = var.network_area_id - name = var.routing_table_name -} - -resource "stackit_routing_table_route" "route" { - organization_id = var.organization_id - network_area_id = var.network_area_id - routing_table_id = stackit_routing_table.routing_table.routing_table_id - destination = { - type = var.destination_type - value = var.destination_value - } - next_hop = { - type = var.next_hop_type - value = var.next_hop_value - } -} diff --git a/stackit/internal/services/iaasalpha/utils/util.go b/stackit/internal/services/iaasalpha/utils/util.go deleted file mode 100644 index 40216b92..00000000 --- a/stackit/internal/services/iaasalpha/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *iaasalpha.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.IaaSCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IaaSCustomEndpoint)) - } - apiClient, err := iaasalpha.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/iaasalpha/utils/util_test.go b/stackit/internal/services/iaasalpha/utils/util_test.go deleted file mode 100644 index d4ba4671..00000000 --- a/stackit/internal/services/iaasalpha/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://iaas-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *iaasalpha.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *iaasalpha.APIClient { - apiClient, err := iaasalpha.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - IaaSCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *iaasalpha.APIClient { - apiClient, err := iaasalpha.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/kms/key/datasource.go b/stackit/internal/services/kms/key/datasource.go deleted file mode 100644 index 97dba197..00000000 --- a/stackit/internal/services/kms/key/datasource.go +++ /dev/null @@ -1,191 +0,0 @@ -package kms - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -var ( - _ datasource.DataSource = &keyDataSource{} -) - -func NewKeyDataSource() datasource.DataSource { - return &keyDataSource{} -} - -type keyDataSource struct { - client *kms.APIClient - providerData core.ProviderData -} - -func (k *keyDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_kms_key" -} - -func (k *keyDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - k.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - k.client = kmsUtils.ConfigureClient(ctx, &k.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "KMS client configured") -} - -func (k *keyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: fmt.Sprintf("KMS Key datasource schema. %s", core.DatasourceRegionFallbackDocstring), - Attributes: map[string]schema.Attribute{ - "access_scope": schema.StringAttribute{ - Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)), - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "algorithm": schema.StringAttribute{ - Description: fmt.Sprintf("The encryption algorithm that the key will use to encrypt data. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAlgorithmEnumValues)...)), - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "description": schema.StringAttribute{ - Description: "A user chosen description to distinguish multiple keys", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "display_name": schema.StringAttribute{ - Description: "The display name to distinguish multiple keys", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`key_id`\".", - Computed: true, - }, - "import_only": schema.BoolAttribute{ - Description: "States whether versions can be created or only imported.", - Computed: true, - }, - "key_id": schema.StringAttribute{ - Description: "The ID of the key", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "keyring_id": schema.StringAttribute{ - Description: "The ID of the associated key ring", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "protection": schema.StringAttribute{ - Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)), - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "purpose": schema.StringAttribute{ - Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedPurposeEnumValues)...)), - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the key is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - }, - } -} - -func (k *keyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := k.providerData.GetRegionWithOverride(model.Region) - keyId := model.KeyId.ValueString() - - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "key_id", keyId) - - keyResponse, err := k.client.GetKey(ctx, projectId, region, keyRingId, keyId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading key", - fmt.Sprintf("Key with ID %q does not exist in project %q.", keyId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(keyResponse, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key read") -} diff --git a/stackit/internal/services/kms/key/resource.go b/stackit/internal/services/kms/key/resource.go deleted file mode 100644 index 9f56669b..00000000 --- a/stackit/internal/services/kms/key/resource.go +++ /dev/null @@ -1,455 +0,0 @@ -package kms - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" - "github.com/stackitcloud/stackit-sdk-go/services/kms/wait" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -const ( - deletionWarning = "Keys will **not** be instantly destroyed by terraform during a `terraform destroy`. They will just be scheduled for deletion via the API and thrown out of the Terraform state afterwards. **This way we can ensure no key setups are deleted by accident and it gives you the option to recover your keys within the grace period.**" -) - -var ( - _ resource.Resource = &keyResource{} - _ resource.ResourceWithConfigure = &keyResource{} - _ resource.ResourceWithImportState = &keyResource{} - _ resource.ResourceWithModifyPlan = &keyResource{} -) - -type Model struct { - AccessScope types.String `tfsdk:"access_scope"` - Algorithm types.String `tfsdk:"algorithm"` - Description types.String `tfsdk:"description"` - DisplayName types.String `tfsdk:"display_name"` - Id types.String `tfsdk:"id"` // needed by TF - ImportOnly types.Bool `tfsdk:"import_only"` - KeyId types.String `tfsdk:"key_id"` - KeyRingId types.String `tfsdk:"keyring_id"` - Protection types.String `tfsdk:"protection"` - Purpose types.String `tfsdk:"purpose"` - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` -} - -func NewKeyResource() resource.Resource { - return &keyResource{} -} - -type keyResource struct { - client *kms.APIClient - providerData core.ProviderData -} - -func (r *keyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_kms_key" -} - -func (r *keyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - r.client = kmsUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "KMS client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *keyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (r *keyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - description := fmt.Sprintf("KMS Key resource schema. %s", core.ResourceRegionFallbackDocstring) - resp.Schema = schema.Schema{ - Description: description, - MarkdownDescription: fmt.Sprintf("%s\n\n ~> %s", description, deletionWarning), - Attributes: map[string]schema.Attribute{ - "access_scope": schema.StringAttribute{ - Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)), - Optional: true, - Computed: true, // must be computed because of default value - Default: stringdefault.StaticString(string(kms.ACCESSSCOPE_PUBLIC)), - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "algorithm": schema.StringAttribute{ - Description: fmt.Sprintf("The encryption algorithm that the key will use to encrypt data. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAlgorithmEnumValues)...)), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "description": schema.StringAttribute{ - Description: "A user chosen description to distinguish multiple keys", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "display_name": schema.StringAttribute{ - Description: "The display name to distinguish multiple keys", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`key_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "import_only": schema.BoolAttribute{ - Description: "States whether versions can be created or only imported.", - Computed: true, - Optional: true, - }, - "key_id": schema.StringAttribute{ - Description: "The ID of the key", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "keyring_id": schema.StringAttribute{ - Description: "The ID of the associated keyring", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "protection": schema.StringAttribute{ - Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "purpose": schema.StringAttribute{ - Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedPurposeEnumValues)...)), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the key is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -func (r *keyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - keyRingId := model.KeyRingId.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - createResponse, err := r.client.CreateKey(ctx, projectId, region, keyRingId).CreateKeyPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if createResponse == nil || createResponse.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", "API returned empty response") - return - } - - keyId := *createResponse.Id - // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": projectId, - "region": region, - "keyring_id": keyRingId, - "key_id": keyId, - }) - - waitHandlerResp, err := wait.CreateOrUpdateKeyWaitHandler(ctx, r.client, projectId, region, keyRingId, keyId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for key creation", fmt.Sprintf("Calling API: %v", err)) - return - } - - err = mapFields(waitHandlerResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key created") -} - -func (r *keyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - keyId := model.KeyId.ValueString() - - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "key_id", keyId) - - keyResponse, err := r.client.GetKey(ctx, projectId, region, keyRingId, keyId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(keyResponse, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key read") -} - -func (r *keyResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // keys cannot be updated, so we log an error. - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key", "Keys can't be updated") -} - -func (r *keyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - keyId := model.KeyId.ValueString() - - err := r.client.DeleteKey(ctx, projectId, region, keyRingId, keyId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting key", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - // The keys can't be deleted instantly by Terraform, they can only be scheduled for deletion via the API. - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Key scheduled for deletion on API side", deletionWarning) - - tflog.Info(ctx, "key deleted") -} - -func (r *keyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing key", - fmt.Sprintf("Exptected import identifier with format: [project_id],[region],[keyring_id],[key_id], got :%q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "keyring_id": idParts[2], - "key_id": idParts[3], - }) - - tflog.Info(ctx, "key state imported") -} - -func mapFields(key *kms.Key, model *Model, region string) error { - if key == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var keyId string - if model.KeyId.ValueString() != "" { - keyId = model.KeyId.ValueString() - } else if key.Id != nil { - keyId = *key.Id - } else { - return fmt.Errorf("key id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.KeyRingId.ValueString(), keyId) - model.KeyId = types.StringValue(keyId) - model.DisplayName = types.StringPointerValue(key.DisplayName) - model.Region = types.StringValue(region) - model.ImportOnly = types.BoolPointerValue(key.ImportOnly) - model.AccessScope = types.StringValue(string(key.GetAccessScope())) - model.Algorithm = types.StringValue(string(key.GetAlgorithm())) - model.Purpose = types.StringValue(string(key.GetPurpose())) - model.Protection = types.StringValue(string(key.GetProtection())) - - // TODO: workaround - remove once STACKITKMS-377 is resolved (just write the return value from the API to the state then) - if !(model.Description.IsNull() && key.Description != nil && *key.Description == "") { - model.Description = types.StringPointerValue(key.Description) - } else { - model.Description = types.StringNull() - } - - return nil -} - -func toCreatePayload(model *Model) (*kms.CreateKeyPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - return &kms.CreateKeyPayload{ - AccessScope: kms.CreateKeyPayloadGetAccessScopeAttributeType(conversion.StringValueToPointer(model.AccessScope)), - Algorithm: kms.CreateKeyPayloadGetAlgorithmAttributeType(conversion.StringValueToPointer(model.Algorithm)), - Protection: kms.CreateKeyPayloadGetProtectionAttributeType(conversion.StringValueToPointer(model.Protection)), - Description: conversion.StringValueToPointer(model.Description), - DisplayName: conversion.StringValueToPointer(model.DisplayName), - ImportOnly: conversion.BoolValueToPointer(model.ImportOnly), - Purpose: kms.CreateKeyPayloadGetPurposeAttributeType(conversion.StringValueToPointer(model.Purpose)), - }, nil -} diff --git a/stackit/internal/services/kms/key/resource_test.go b/stackit/internal/services/kms/key/resource_test.go deleted file mode 100644 index f4846eca..00000000 --- a/stackit/internal/services/kms/key/resource_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package kms - -import ( - "fmt" - "testing" - - "github.com/google/uuid" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/kms" -) - -var ( - keyId = uuid.NewString() - keyRingId = uuid.NewString() - projectId = uuid.NewString() -) - -func TestMapFields(t *testing.T) { - type args struct { - state Model - input *kms.Key - region string - } - tests := []struct { - description string - args args - expected Model - isValid bool - }{ - { - description: "default values", - args: args{ - state: Model{ - KeyId: types.StringValue(keyId), - KeyRingId: types.StringValue(keyRingId), - ProjectId: types.StringValue(projectId), - }, - input: &kms.Key{ - Id: utils.Ptr(keyId), - Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), - Algorithm: utils.Ptr(kms.ALGORITHM_ECDSA_P256_SHA256), - Purpose: utils.Ptr(kms.PURPOSE_ASYMMETRIC_SIGN_VERIFY), - AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), - }, - region: "eu01", - }, - expected: Model{ - Description: types.StringNull(), - DisplayName: types.StringNull(), - KeyRingId: types.StringValue(keyRingId), - KeyId: types.StringValue(keyId), - Id: types.StringValue(fmt.Sprintf("%s,eu01,%s,%s", projectId, keyRingId, keyId)), - ProjectId: types.StringValue(projectId), - Region: types.StringValue("eu01"), - Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), - Algorithm: types.StringValue(string(kms.ALGORITHM_ECDSA_P256_SHA256)), - Purpose: types.StringValue(string(kms.PURPOSE_ASYMMETRIC_SIGN_VERIFY)), - AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)), - }, - isValid: true, - }, - { - description: "values_ok", - args: args{ - state: Model{ - KeyId: types.StringValue(keyId), - KeyRingId: types.StringValue(keyRingId), - ProjectId: types.StringValue(projectId), - }, - input: &kms.Key{ - Id: utils.Ptr(keyId), - Description: utils.Ptr("descr"), - DisplayName: utils.Ptr("name"), - ImportOnly: utils.Ptr(true), - Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), - Algorithm: utils.Ptr(kms.ALGORITHM_AES_256_GCM), - Purpose: utils.Ptr(kms.PURPOSE_MESSAGE_AUTHENTICATION_CODE), - AccessScope: utils.Ptr(kms.ACCESSSCOPE_SNA), - }, - region: "eu01", - }, - expected: Model{ - Description: types.StringValue("descr"), - DisplayName: types.StringValue("name"), - KeyId: types.StringValue(keyId), - KeyRingId: types.StringValue(keyRingId), - Id: types.StringValue(fmt.Sprintf("%s,eu01,%s,%s", projectId, keyRingId, keyId)), - ProjectId: types.StringValue(projectId), - Region: types.StringValue("eu01"), - ImportOnly: types.BoolValue(true), - Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), - Algorithm: types.StringValue(string(kms.ALGORITHM_AES_256_GCM)), - Purpose: types.StringValue(string(kms.PURPOSE_MESSAGE_AUTHENTICATION_CODE)), - AccessScope: types.StringValue(string(kms.ACCESSSCOPE_SNA)), - }, - isValid: true, - }, - { - description: "nil_response_field", - args: args{ - state: Model{}, - input: &kms.Key{ - Id: nil, - }, - }, - expected: Model{}, - isValid: false, - }, - { - description: "nil_response", - args: args{ - state: Model{}, - input: nil, - }, - expected: Model{}, - isValid: false, - }, - { - description: "no_resource_id", - args: args{ - state: Model{ - Region: types.StringValue("eu01"), - ProjectId: types.StringValue(projectId), - }, - input: &kms.Key{}, - }, - expected: Model{}, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - KeyRingId: tt.expected.KeyRingId, - } - err := mapFields(tt.args.input, state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *kms.CreateKeyPayload - isValid bool - }{ - { - description: "default_values", - input: &Model{}, - expected: &kms.CreateKeyPayload{}, - isValid: true, - }, - { - description: "simple_values", - input: &Model{ - DisplayName: types.StringValue("name"), - }, - expected: &kms.CreateKeyPayload{ - DisplayName: utils.Ptr("name"), - }, - isValid: true, - }, - { - description: "null_fields", - input: &Model{ - DisplayName: types.StringValue(""), - Description: types.StringValue(""), - }, - expected: &kms.CreateKeyPayload{ - DisplayName: utils.Ptr(""), - Description: utils.Ptr(""), - }, - isValid: true, - }, - { - description: "nil_model", - input: nil, - expected: nil, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/kms/keyring/datasource.go b/stackit/internal/services/kms/keyring/datasource.go deleted file mode 100644 index a89944bf..00000000 --- a/stackit/internal/services/kms/keyring/datasource.go +++ /dev/null @@ -1,144 +0,0 @@ -package kms - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -var ( - _ datasource.DataSource = &keyRingDataSource{} -) - -func NewKeyRingDataSource() datasource.DataSource { - return &keyRingDataSource{} -} - -type keyRingDataSource struct { - client *kms.APIClient - providerData core.ProviderData -} - -func (k *keyRingDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { - response.TypeName = request.ProviderTypeName + "_kms_keyring" -} - -func (k *keyRingDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { - var ok bool - k.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - apiClient := kmsUtils.ConfigureClient(ctx, &k.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - - k.client = apiClient - tflog.Info(ctx, "KMS client configured") -} - -func (k *keyRingDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { - response.Schema = schema.Schema{ - Description: fmt.Sprintf("KMS Keyring datasource schema. %s", core.DatasourceRegionFallbackDocstring), - Attributes: map[string]schema.Attribute{ - "description": schema.StringAttribute{ - Description: "A user chosen description to distinguish multiple keyrings.", - Computed: true, - }, - "display_name": schema.StringAttribute{ - Description: "The display name to distinguish multiple keyrings.", - Computed: true, - }, - "keyring_id": schema.StringAttribute{ - Description: "An auto generated unique id which identifies the keyring.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the keyring is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - }, - } -} - -func (k *keyRingDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - - diags := request.Config.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := k.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - keyRingResponse, err := k.client.GetKeyRing(ctx, projectId, region, keyRingId).Execute() - if err != nil { - utils.LogError( - ctx, - &response.Diagnostics, - err, - "Reading keyring", - fmt.Sprintf("Keyring with ID %q does not exist in project %q.", keyRingId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - response.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(keyRingResponse, &model, region) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading keyring", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = response.State.Set(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key ring read") -} diff --git a/stackit/internal/services/kms/keyring/resource.go b/stackit/internal/services/kms/keyring/resource.go deleted file mode 100644 index 3627f3f9..00000000 --- a/stackit/internal/services/kms/keyring/resource.go +++ /dev/null @@ -1,361 +0,0 @@ -package kms - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/stackit-sdk-go/services/kms/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -const ( - deletionWarning = "Keyrings will **not** be destroyed by terraform during a `terraform destroy`. They will just be thrown out of the Terraform state and not deleted on API side. **This way we can ensure no keyring setups are deleted by accident and it gives you the option to recover your keys within the grace period.**" -) - -var ( - _ resource.Resource = &keyRingResource{} - _ resource.ResourceWithConfigure = &keyRingResource{} - _ resource.ResourceWithImportState = &keyRingResource{} - _ resource.ResourceWithModifyPlan = &keyRingResource{} -) - -type Model struct { - Description types.String `tfsdk:"description"` - DisplayName types.String `tfsdk:"display_name"` - KeyRingId types.String `tfsdk:"keyring_id"` - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` -} - -func NewKeyRingResource() resource.Resource { - return &keyRingResource{} -} - -type keyRingResource struct { - client *kms.APIClient - providerData core.ProviderData -} - -func (r *keyRingResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { - response.TypeName = request.ProviderTypeName + "_kms_keyring" -} - -func (r *keyRingResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - r.client = kmsUtils.ConfigureClient(ctx, &r.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "KMS client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *keyRingResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (r *keyRingResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { - description := fmt.Sprintf("KMS Keyring resource schema. %s", core.ResourceRegionFallbackDocstring) - - response.Schema = schema.Schema{ - Description: description, - MarkdownDescription: fmt.Sprintf("%s\n\n ~> %s", description, deletionWarning), - Attributes: map[string]schema.Attribute{ - "description": schema.StringAttribute{ - Description: "A user chosen description to distinguish multiple keyrings.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "display_name": schema.StringAttribute{ - Description: "The display name to distinguish multiple keyrings.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "keyring_id": schema.StringAttribute{ - Description: "An auto generated unique id which identifies the keyring.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the keyring is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -func (r *keyRingResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating keyring", fmt.Sprintf("Creating API payload: %v", err)) - return - } - createResponse, err := r.client.CreateKeyRing(ctx, projectId, region).CreateKeyRingPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating keyring", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if createResponse == nil || createResponse.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating keyring", "API returned empty response") - return - } - - keyRingId := *createResponse.Id - // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": projectId, - "region": region, - "keyring_id": keyRingId, - }) - - waitResp, err := wait.CreateKeyRingWaitHandler(ctx, r.client, projectId, region, keyRingId).SetSleepBeforeWait(5 * time.Second).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating keyring", fmt.Sprintf("Key Ring creation waiting: %v", err)) - return - } - - err = mapFields(waitResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating keyring", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key Ring created") -} - -func (r *keyRingResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - keyRingResponse, err := r.client.GetKeyRing(ctx, projectId, region, keyRingId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading keyring", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(keyRingResponse, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading keyring", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key ring read") -} - -func (r *keyRingResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // keyrings cannot be updated, so we log an error. - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating keyring", "Keyrings can't be updated") -} - -func (r *keyRingResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // The keyring can't be deleted by Terraform because it potentially has still keys inside it. - // These keys might be *scheduled* for deletion, but aren't deleted completely, so the delete request would fail. - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Keyring not deleted on API side", deletionWarning) - - tflog.Info(ctx, "keyring deleted") -} - -func (r *keyRingResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing keyring", - fmt.Sprintf("Exptected import identifier with format: [project_id],[region],[keyring_id], got :%q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "keyring_id": idParts[2], - }) - - tflog.Info(ctx, "keyring state imported") -} - -func mapFields(keyRing *kms.KeyRing, model *Model, region string) error { - if keyRing == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var keyRingId string - if model.KeyRingId.ValueString() != "" { - keyRingId = model.KeyRingId.ValueString() - } else if keyRing.Id != nil { - keyRingId = *keyRing.Id - } else { - return fmt.Errorf("keyring id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, keyRingId) - model.KeyRingId = types.StringValue(keyRingId) - model.DisplayName = types.StringPointerValue(keyRing.DisplayName) - model.Region = types.StringValue(region) - - // TODO: workaround - remove once STACKITKMS-377 is resolved (just write the return value from the API to the state then) - if !(model.Description.IsNull() && keyRing.Description != nil && *keyRing.Description == "") { - model.Description = types.StringPointerValue(keyRing.Description) - } else { - model.Description = types.StringNull() - } - - return nil -} - -func toCreatePayload(model *Model) (*kms.CreateKeyRingPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &kms.CreateKeyRingPayload{ - Description: conversion.StringValueToPointer(model.Description), - DisplayName: conversion.StringValueToPointer(model.DisplayName), - }, nil -} diff --git a/stackit/internal/services/kms/keyring/resource_test.go b/stackit/internal/services/kms/keyring/resource_test.go deleted file mode 100644 index 9645c5f0..00000000 --- a/stackit/internal/services/kms/keyring/resource_test.go +++ /dev/null @@ -1,173 +0,0 @@ -package kms - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/kms" -) - -const testRegion = "eu01" - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *kms.KeyRing - expected Model - isValid bool - }{ - { - "default values", - Model{ - KeyRingId: types.StringValue("krid"), - ProjectId: types.StringValue("pid"), - }, - &kms.KeyRing{ - Id: utils.Ptr("krid"), - }, - Model{ - Description: types.StringNull(), - DisplayName: types.StringNull(), - KeyRingId: types.StringValue("krid"), - Id: types.StringValue("pid,eu01,krid"), - ProjectId: types.StringValue("pid"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "values_ok", - Model{ - KeyRingId: types.StringValue("krid"), - ProjectId: types.StringValue("pid"), - }, - &kms.KeyRing{ - Description: utils.Ptr("descr"), - DisplayName: utils.Ptr("name"), - Id: utils.Ptr("krid"), - }, - Model{ - Description: types.StringValue("descr"), - DisplayName: types.StringValue("name"), - KeyRingId: types.StringValue("krid"), - Id: types.StringValue("pid,eu01,krid"), - ProjectId: types.StringValue("pid"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response_field", - Model{}, - &kms.KeyRing{ - Id: nil, - }, - Model{}, - false, - }, - { - "nil_response", - Model{}, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - Region: types.StringValue(testRegion), - ProjectId: types.StringValue("pid"), - }, - &kms.KeyRing{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - KeyRingId: tt.expected.KeyRingId, - } - err := mapFields(tt.input, state, testRegion) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - fmt.Println("state: ", state, " expected: ", tt.expected) - t.Fatalf("Data does not match") - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *kms.CreateKeyRingPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &kms.CreateKeyRingPayload{}, - true, - }, - { - "simple_values", - &Model{ - DisplayName: types.StringValue("name"), - }, - &kms.CreateKeyRingPayload{ - DisplayName: utils.Ptr("name"), - }, - true, - }, - { - "null_fields", - &Model{ - DisplayName: types.StringValue(""), - Description: types.StringValue(""), - }, - &kms.CreateKeyRingPayload{ - DisplayName: utils.Ptr(""), - Description: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/kms/kms_acc_test.go b/stackit/internal/services/kms/kms_acc_test.go deleted file mode 100644 index bea3e452..00000000 --- a/stackit/internal/services/kms/kms_acc_test.go +++ /dev/null @@ -1,1035 +0,0 @@ -package kms_test - -import ( - "context" - _ "embed" - "errors" - "fmt" - "maps" - "net/http" - "strings" - "sync" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/plancheck" - coreConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/keyring-min.tf - resourceKeyRingMinConfig string - - //go:embed testdata/keyring-max.tf - resourceKeyRingMaxConfig string - - //go:embed testdata/key-min.tf - resourceKeyMinConfig string - - //go:embed testdata/key-max.tf - resourceKeyMaxConfig string - - //go:embed testdata/wrapping-key-min.tf - resourceWrappingKeyMinConfig string - - //go:embed testdata/wrapping-key-max.tf - resourceWrappingKeyMaxConfig string -) - -// KEY RING - MIN - -var testConfigKeyRingVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), -} - -var testConfigKeyRingVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigKeyRingVarsMin) - updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) - return updatedConfig -} - -// KEY RING - MAX - -var testConfigKeyRingVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "description": config.StringVariable("description"), - "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), -} - -var testConfigKeyRingVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigKeyRingVarsMax) - updatedConfig["description"] = config.StringVariable("updated description") - updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) - return updatedConfig -} - -// KEY - MIN - -var testConfigKeyVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "algorithm": config.StringVariable(string(kms.ALGORITHM_AES_256_GCM)), - "protection": config.StringVariable("software"), - "purpose": config.StringVariable(string(kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT)), -} - -var testConfigKeyVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigKeyVarsMin) - updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) - updatedConfig["algorithm"] = config.StringVariable(string(kms.ALGORITHM_RSA_3072_OAEP_SHA256)) - updatedConfig["purpose"] = config.StringVariable(string(kms.PURPOSE_ASYMMETRIC_ENCRYPT_DECRYPT)) - return updatedConfig -} - -// KEY - MAX - -var testConfigKeyVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "algorithm": config.StringVariable(string(kms.ALGORITHM_AES_256_GCM)), - "protection": config.StringVariable("software"), - "purpose": config.StringVariable(string(kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT)), - "access_scope": config.StringVariable(string(kms.ACCESSSCOPE_PUBLIC)), - "import_only": config.BoolVariable(true), - "description": config.StringVariable("kms-key-description"), -} - -var testConfigKeyVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigKeyVarsMax) - updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) - updatedConfig["algorithm"] = config.StringVariable(string(kms.ALGORITHM_RSA_3072_OAEP_SHA256)) - updatedConfig["purpose"] = config.StringVariable(string(kms.PURPOSE_ASYMMETRIC_ENCRYPT_DECRYPT)) - updatedConfig["import_only"] = config.BoolVariable(true) - updatedConfig["description"] = config.StringVariable("kms-key-description-updated") - return updatedConfig -} - -// WRAPPING KEY - MIN - -var testConfigWrappingKeyVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "algorithm": config.StringVariable(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), - "protection": config.StringVariable(string(kms.PROTECTION_SOFTWARE)), - "purpose": config.StringVariable(string(kms.WRAPPINGPURPOSE_SYMMETRIC_KEY)), -} - -var testConfigWrappingKeyVarsMinUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigWrappingKeyVarsMin) - updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) - updatedConfig["algorithm"] = config.StringVariable(string(kms.WRAPPINGALGORITHM__4096_OAEP_SHA256_AES_256_KEY_WRAP)) - updatedConfig["purpose"] = config.StringVariable(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)) - return updatedConfig -} - -// WRAPPING KEY - MAX - -var testConfigWrappingKeyVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "keyring_display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "display_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "algorithm": config.StringVariable(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), - "protection": config.StringVariable(string(kms.PROTECTION_SOFTWARE)), - "purpose": config.StringVariable(string(kms.WRAPPINGPURPOSE_SYMMETRIC_KEY)), - "description": config.StringVariable("kms-wrapping-key-description"), - "access_scope": config.StringVariable(string(kms.ACCESSSCOPE_PUBLIC)), -} - -var testConfigWrappingKeyVarsMaxUpdated = func() config.Variables { - updatedConfig := config.Variables{} - maps.Copy(updatedConfig, testConfigWrappingKeyVarsMax) - updatedConfig["display_name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["display_name"]))) - updatedConfig["algorithm"] = config.StringVariable(string(kms.WRAPPINGALGORITHM__4096_OAEP_SHA256_AES_256_KEY_WRAP)) - updatedConfig["purpose"] = config.StringVariable(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)) - updatedConfig["description"] = config.StringVariable("kms-wrapping-key-description-updated") - return updatedConfig -} - -func TestAccKeyRingMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigKeyRingVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMin["display_name"])), - resource.TestCheckResourceAttrSet("stackit_kms_keyring.keyring", "keyring_id"), - resource.TestCheckNoResourceAttr("stackit_kms_keyring.keyring", "description"), - ), - }, - // Data source - { - ConfigVariables: testConfigKeyRingVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_kms_keyring" "keyring" { - project_id = stackit_kms_keyring.keyring.project_id - keyring_id = stackit_kms_keyring.keyring.keyring_id - } - `, - testutil.KMSProviderConfig(), resourceKeyRingMinConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_kms_keyring.keyring", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("data.stackit_kms_keyring.keyring", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "data.stackit_kms_keyring.keyring", "keyring_id", - ), - resource.TestCheckResourceAttr("data.stackit_kms_keyring.keyring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMin["display_name"])), - resource.TestCheckNoResourceAttr("data.stackit_kms_keyring.keyring", "description"), - ), - }, - // Import - { - ConfigVariables: testConfigKeyRingVarsMin, - ResourceName: "stackit_kms_keyring.keyring", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_kms_keyring.keyring"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_kms_keyring.keyring") - } - keyRingId, ok := r.Primary.Attributes["keyring_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute keyring_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigKeyRingVarsMinUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionReplace), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMinUpdated()["display_name"])), - resource.TestCheckResourceAttrSet("stackit_kms_keyring.keyring", "keyring_id"), - resource.TestCheckNoResourceAttr("stackit_kms_keyring.keyring", "description"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccKeyRingMax(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigKeyRingVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "description", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["description"])), - resource.TestCheckResourceAttrSet("stackit_kms_keyring.keyring", "keyring_id"), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["display_name"])), - ), - }, - // Data Source - { - ConfigVariables: testConfigKeyRingVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_kms_keyring" "keyring" { - project_id = stackit_kms_keyring.keyring.project_id - keyring_id = stackit_kms_keyring.keyring.keyring_id - } - `, - testutil.KMSProviderConfig(), resourceKeyRingMaxConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_kms_keyring.keyring", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("data.stackit_kms_keyring.keyring", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "data.stackit_kms_keyring.keyring", "keyring_id", - ), - resource.TestCheckResourceAttr("data.stackit_kms_keyring.keyring", "description", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_kms_keyring.keyring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMax["display_name"])), - ), - ), - }, - // Import - { - ConfigVariables: testConfigKeyRingVarsMax, - ResourceName: "stackit_kms_keyring.keyring", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_kms_keyring.keyring"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_kms_keyring.keyring") - } - keyRingId, ok := r.Primary.Attributes["keyring_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute keyring_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigKeyRingVarsMaxUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyRingMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionReplace), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "region", testutil.Region), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "display_name", testutil.ConvertConfigVariable(testConfigKeyRingVarsMaxUpdated()["display_name"])), - resource.TestCheckResourceAttrSet("stackit_kms_keyring.keyring", "keyring_id"), - resource.TestCheckResourceAttr("stackit_kms_keyring.keyring", "description", testutil.ConvertConfigVariable(testConfigKeyRingVarsMaxUpdated()["description"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccKeyMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigKeyVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), - plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_key.key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"), - resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMin["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMin["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMin["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMin["protection"])), - resource.TestCheckNoResourceAttr("stackit_kms_key.key", "description"), - resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), - resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", "false"), - ), - }, - // Data Source - { - ConfigVariables: testConfigKeyVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_kms_key" "key" { - project_id = stackit_kms_key.key.project_id - keyring_id = stackit_kms_key.key.keyring_id - key_id = stackit_kms_key.key.key_id - } - `, - testutil.KMSProviderConfig(), resourceKeyMinConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "data.stackit_kms_key.key", "keyring_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_kms_key.key", "key_id", - "data.stackit_kms_key.key", "key_id", - ), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMin["algorithm"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMin["display_name"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMin["purpose"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMin["protection"])), - resource.TestCheckNoResourceAttr("data.stackit_kms_key.key", "description"), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "import_only", "false"), - ), - ), - }, - // Import - { - ConfigVariables: testConfigKeyVarsMin, - ResourceName: "stackit_kms_key.key", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_kms_key.key"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_kms_key.key") - } - keyRingId, ok := r.Primary.Attributes["keyring_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute keyring_id") - } - keyId, ok := r.Primary.Attributes["key_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute key_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, keyId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigKeyVarsMinUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionReplace), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_key.key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"), - resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMinUpdated()["protection"])), - resource.TestCheckNoResourceAttr("stackit_kms_key.key", "description"), - resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), - resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", "false"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccKeyMax(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigKeyVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), - plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_key.key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"), - resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMax["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMax["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMax["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMax["protection"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "description", testutil.ConvertConfigVariable(testConfigKeyVarsMax["description"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", testutil.ConvertConfigVariable(testConfigKeyVarsMax["access_scope"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", testutil.ConvertConfigVariable(testConfigKeyVarsMax["import_only"])), - ), - }, - // Data Source - { - ConfigVariables: testConfigKeyVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_kms_key" "key" { - project_id = stackit_kms_key.key.project_id - keyring_id = stackit_kms_key.key.keyring_id - key_id = stackit_kms_key.key.key_id - } - `, - testutil.KMSProviderConfig(), resourceKeyMaxConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "data.stackit_kms_key.key", "keyring_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_kms_key.key", "key_id", - "data.stackit_kms_key.key", "key_id", - ), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMax["algorithm"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMax["display_name"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMax["purpose"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMax["protection"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "description", testutil.ConvertConfigVariable(testConfigKeyVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "access_scope", testutil.ConvertConfigVariable(testConfigKeyVarsMax["access_scope"])), - resource.TestCheckResourceAttr("data.stackit_kms_key.key", "import_only", testutil.ConvertConfigVariable(testConfigKeyVarsMax["import_only"])), - ), - ), - }, - // Import - { - ConfigVariables: testConfigKeyVarsMax, - ResourceName: "stackit_kms_key.key", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_kms_key.key"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_kms_key.key") - } - keyRingId, ok := r.Primary.Attributes["keyring_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute keyring_id") - } - keyId, ok := r.Primary.Attributes["key_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute key_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, keyId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigKeyVarsMaxUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceKeyMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_key.key", plancheck.ResourceActionReplace), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_key.key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_key.key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_key.key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_key.key", "key_id"), - resource.TestCheckResourceAttr("stackit_kms_key.key", "algorithm", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "display_name", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "purpose", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "protection", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["protection"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "description", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["description"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "access_scope", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["access_scope"])), - resource.TestCheckResourceAttr("stackit_kms_key.key", "import_only", testutil.ConvertConfigVariable(testConfigKeyVarsMaxUpdated()["import_only"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccWrappingKeyMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigWrappingKeyVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), - plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_wrapping_key.wrapping_key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["protection"])), - resource.TestCheckNoResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description"), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), - ), - }, - // Data Source - { - ConfigVariables: testConfigWrappingKeyVarsMin, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_kms_wrapping_key" "wrapping_key" { - project_id = stackit_kms_wrapping_key.wrapping_key.project_id - keyring_id = stackit_kms_wrapping_key.wrapping_key.keyring_id - wrapping_key_id = stackit_kms_wrapping_key.wrapping_key.wrapping_key_id - } - `, - testutil.KMSProviderConfig(), resourceWrappingKeyMinConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "data.stackit_kms_wrapping_key.wrapping_key", "keyring_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", - "data.stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", - ), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["algorithm"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["display_name"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["purpose"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMin["protection"])), - resource.TestCheckNoResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "description"), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), - resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "public_key"), - resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "expires_at"), - resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "created_at"), - ), - ), - }, - // Import - { - ConfigVariables: testConfigWrappingKeyVarsMin, - ResourceName: "stackit_kms_wrapping_key.wrapping_key", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_kms_wrapping_key.wrapping_key"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_kms_wrapping_key.wrapping_key") - } - keyRingId, ok := r.Primary.Attributes["keyring_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute keyring_id") - } - wrappingKeyId, ok := r.Primary.Attributes["wrapping_key_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute wrapping_key_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, wrappingKeyId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigWrappingKeyVarsMinUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMinConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionReplace), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_wrapping_key.wrapping_key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMinUpdated()["protection"])), - resource.TestCheckNoResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description"), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", string(kms.ACCESSSCOPE_PUBLIC)), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccWrappingKeyMax(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigWrappingKeyVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionCreate), - plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionCreate), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_wrapping_key.wrapping_key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["protection"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["description"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["access_scope"])), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), - ), - }, - // Data Source - { - ConfigVariables: testConfigWrappingKeyVarsMax, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_kms_wrapping_key" "wrapping_key" { - project_id = stackit_kms_wrapping_key.wrapping_key.project_id - keyring_id = stackit_kms_wrapping_key.wrapping_key.keyring_id - wrapping_key_id = stackit_kms_wrapping_key.wrapping_key.wrapping_key_id - } - `, - testutil.KMSProviderConfig(), resourceWrappingKeyMaxConfig, - ), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionNoop), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "data.stackit_kms_wrapping_key.wrapping_key", "keyring_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", - "data.stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id", - ), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["algorithm"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["display_name"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["purpose"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["protection"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["description"])), - resource.TestCheckResourceAttr("data.stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMax["access_scope"])), - resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "public_key"), - resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "expires_at"), - resource.TestCheckResourceAttrSet("data.stackit_kms_wrapping_key.wrapping_key", "created_at"), - ), - ), - }, - // Import - { - ConfigVariables: testConfigWrappingKeyVarsMax, - ResourceName: "stackit_kms_wrapping_key.wrapping_key", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_kms_wrapping_key.wrapping_key"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_kms_wrapping_key.wrapping_key") - } - keyRingId, ok := r.Primary.Attributes["keyring_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute keyring_id") - } - wrappingKeyId, ok := r.Primary.Attributes["wrapping_key_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute wrapping_key_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, keyRingId, wrappingKeyId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testConfigWrappingKeyVarsMaxUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.KMSProviderConfig(), resourceWrappingKeyMaxConfig), - ConfigPlanChecks: resource.ConfigPlanChecks{ - PreApply: []plancheck.PlanCheck{ - plancheck.ExpectResourceAction("stackit_kms_keyring.keyring", plancheck.ResourceActionNoop), - plancheck.ExpectResourceAction("stackit_kms_wrapping_key.wrapping_key", plancheck.ResourceActionReplace), - }, - }, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "project_id", testutil.ProjectId), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "region", testutil.Region), - resource.TestCheckResourceAttrPair( - "stackit_kms_keyring.keyring", "keyring_id", - "stackit_kms_wrapping_key.wrapping_key", "keyring_id", - ), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "wrapping_key_id"), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "algorithm", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["algorithm"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "display_name", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["display_name"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "purpose", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["purpose"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "protection", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["protection"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "description", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["description"])), - resource.TestCheckResourceAttr("stackit_kms_wrapping_key.wrapping_key", "access_scope", testutil.ConvertConfigVariable(testConfigWrappingKeyVarsMaxUpdated()["access_scope"])), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "public_key"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "expires_at"), - resource.TestCheckResourceAttrSet("stackit_kms_wrapping_key.wrapping_key", "created_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckDestroy(s *terraform.State) error { - checkFunctions := []func(s *terraform.State) error{ - testAccCheckKeyDestroy, - testAccCheckWrappingKeyDestroy, - testAccCheckKeyRingDestroy, - } - - var errs []error - - wg := sync.WaitGroup{} - wg.Add(len(checkFunctions)) - - for _, f := range checkFunctions { - go func() { - err := f(s) - if err != nil { - errs = append(errs, err) - } - wg.Done() - }() - } - wg.Wait() - return errors.Join(errs...) -} - -func testAccCheckKeyRingDestroy(s *terraform.State) error { - ctx := context.Background() - var client *kms.APIClient - var err error - if testutil.KMSCustomEndpoint == "" { - client, err = kms.NewAPIClient() - } else { - client, err = kms.NewAPIClient( - coreConfig.WithEndpoint(testutil.KMSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_kms_keyring" { - continue - } - keyRingId := strings.Split(rs.Primary.ID, core.Separator)[2] - err := client.DeleteKeyRingExecute(ctx, testutil.ProjectId, testutil.Region, keyRingId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - continue - } - - // Workaround: when the delete endpoint is called for a keyring which has keys inside it (no matter if - // they are scheduled for deletion or not, it will throw an HTTP 400 error and the keyring can't be - // deleted then). - // But at least we can delete all empty keyrings created by the keyring acc tests this way. - if oapiErr.StatusCode == http.StatusBadRequest { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger keyring deletion %q: %w", keyRingId, err)) - } - } - - return errors.Join(errs...) -} - -func testAccCheckKeyDestroy(s *terraform.State) error { - ctx := context.Background() - var client *kms.APIClient - var err error - if testutil.KMSCustomEndpoint == "" { - client, err = kms.NewAPIClient() - } else { - client, err = kms.NewAPIClient( - coreConfig.WithEndpoint(testutil.KMSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_kms_key" { - continue - } - keyRingId := strings.Split(rs.Primary.ID, core.Separator)[2] - keyId := strings.Split(rs.Primary.ID, core.Separator)[3] - err := client.DeleteKeyExecute(ctx, testutil.ProjectId, testutil.Region, keyRingId, keyId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - continue - } - - // workaround: when the delete endpoint is called a second time for a key which is already scheduled - // for deletion, one will get an HTTP 400 error which we have to ignore here - if oapiErr.StatusCode == http.StatusBadRequest { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger key deletion %q: %w", keyRingId, err)) - } - } - - return errors.Join(errs...) -} - -func testAccCheckWrappingKeyDestroy(s *terraform.State) error { - ctx := context.Background() - var client *kms.APIClient - var err error - if testutil.KMSCustomEndpoint == "" { - client, err = kms.NewAPIClient() - } else { - client, err = kms.NewAPIClient( - coreConfig.WithEndpoint(testutil.KMSCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var errs []error - - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_kms_wrapping_key" { - continue - } - keyRingId := strings.Split(rs.Primary.ID, core.Separator)[2] - wrappingKeyId := strings.Split(rs.Primary.ID, core.Separator)[3] - err := client.DeleteWrappingKeyExecute(ctx, testutil.ProjectId, testutil.Region, keyRingId, wrappingKeyId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - continue - } - } - errs = append(errs, fmt.Errorf("cannot trigger wrapping key deletion %q: %w", keyRingId, err)) - } - } - - return errors.Join(errs...) -} diff --git a/stackit/internal/services/kms/testdata/key-max.tf b/stackit/internal/services/kms/testdata/key-max.tf deleted file mode 100644 index 2264f030..00000000 --- a/stackit/internal/services/kms/testdata/key-max.tf +++ /dev/null @@ -1,27 +0,0 @@ -variable "project_id" {} - -variable "keyring_display_name" {} -variable "display_name" {} -variable "description" {} -variable "access_scope" {} -variable "import_only" {} -variable "protection" {} -variable "algorithm" {} -variable "purpose" {} - -resource "stackit_kms_keyring" "keyring" { - project_id = var.project_id - display_name = var.keyring_display_name -} - -resource "stackit_kms_key" "key" { - project_id = var.project_id - keyring_id = stackit_kms_keyring.keyring.keyring_id - protection = var.protection - algorithm = var.algorithm - display_name = var.display_name - purpose = var.purpose - description = var.description - access_scope = var.access_scope - import_only = var.import_only -} diff --git a/stackit/internal/services/kms/testdata/key-min.tf b/stackit/internal/services/kms/testdata/key-min.tf deleted file mode 100644 index 04c1897f..00000000 --- a/stackit/internal/services/kms/testdata/key-min.tf +++ /dev/null @@ -1,21 +0,0 @@ -variable "project_id" {} - -variable "keyring_display_name" {} -variable "display_name" {} -variable "protection" {} -variable "algorithm" {} -variable "purpose" {} - -resource "stackit_kms_keyring" "keyring" { - project_id = var.project_id - display_name = var.keyring_display_name -} - -resource "stackit_kms_key" "key" { - project_id = var.project_id - keyring_id = stackit_kms_keyring.keyring.keyring_id - protection = var.protection - algorithm = var.algorithm - display_name = var.display_name - purpose = var.purpose -} diff --git a/stackit/internal/services/kms/testdata/keyring-max.tf b/stackit/internal/services/kms/testdata/keyring-max.tf deleted file mode 100644 index 74497f67..00000000 --- a/stackit/internal/services/kms/testdata/keyring-max.tf +++ /dev/null @@ -1,10 +0,0 @@ -variable "project_id" {} - -variable "display_name" {} -variable "description" {} - -resource "stackit_kms_keyring" "keyring" { - project_id = var.project_id - display_name = var.display_name - description = var.description -} diff --git a/stackit/internal/services/kms/testdata/keyring-min.tf b/stackit/internal/services/kms/testdata/keyring-min.tf deleted file mode 100644 index cb38cad3..00000000 --- a/stackit/internal/services/kms/testdata/keyring-min.tf +++ /dev/null @@ -1,8 +0,0 @@ -variable "project_id" {} - -variable "display_name" {} - -resource "stackit_kms_keyring" "keyring" { - project_id = var.project_id - display_name = var.display_name -} diff --git a/stackit/internal/services/kms/testdata/wrapping-key-max.tf b/stackit/internal/services/kms/testdata/wrapping-key-max.tf deleted file mode 100644 index 0b461ed0..00000000 --- a/stackit/internal/services/kms/testdata/wrapping-key-max.tf +++ /dev/null @@ -1,25 +0,0 @@ -variable "project_id" {} - -variable "keyring_display_name" {} -variable "display_name" {} -variable "protection" {} -variable "algorithm" {} -variable "purpose" {} -variable "description" {} -variable "access_scope" {} - -resource "stackit_kms_keyring" "keyring" { - project_id = var.project_id - display_name = var.keyring_display_name -} - -resource "stackit_kms_wrapping_key" "wrapping_key" { - project_id = var.project_id - keyring_id = stackit_kms_keyring.keyring.keyring_id - protection = var.protection - algorithm = var.algorithm - display_name = var.display_name - purpose = var.purpose - description = var.description - access_scope = var.access_scope -} diff --git a/stackit/internal/services/kms/testdata/wrapping-key-min.tf b/stackit/internal/services/kms/testdata/wrapping-key-min.tf deleted file mode 100644 index 3a5df17f..00000000 --- a/stackit/internal/services/kms/testdata/wrapping-key-min.tf +++ /dev/null @@ -1,21 +0,0 @@ -variable "project_id" {} - -variable "keyring_display_name" {} -variable "display_name" {} -variable "protection" {} -variable "algorithm" {} -variable "purpose" {} - -resource "stackit_kms_keyring" "keyring" { - project_id = var.project_id - display_name = var.keyring_display_name -} - -resource "stackit_kms_wrapping_key" "wrapping_key" { - project_id = var.project_id - keyring_id = stackit_kms_keyring.keyring.keyring_id - protection = var.protection - algorithm = var.algorithm - display_name = var.display_name - purpose = var.purpose -} diff --git a/stackit/internal/services/kms/utils/util.go b/stackit/internal/services/kms/utils/util.go deleted file mode 100644 index 9f6f64d8..00000000 --- a/stackit/internal/services/kms/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *kms.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.KMSCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.KMSCustomEndpoint)) - } - apiClient, err := kms.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/kms/wrapping-key/datasource.go b/stackit/internal/services/kms/wrapping-key/datasource.go deleted file mode 100644 index 396e6f12..00000000 --- a/stackit/internal/services/kms/wrapping-key/datasource.go +++ /dev/null @@ -1,179 +0,0 @@ -package kms - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -var ( - _ datasource.DataSource = &wrappingKeyDataSource{} -) - -func NewWrappingKeyDataSource() datasource.DataSource { - return &wrappingKeyDataSource{} -} - -type wrappingKeyDataSource struct { - client *kms.APIClient - providerData core.ProviderData -} - -func (w *wrappingKeyDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { - response.TypeName = request.ProviderTypeName + "_kms_wrapping_key" -} - -func (w *wrappingKeyDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { - var ok bool - w.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - w.client = kmsUtils.ConfigureClient(ctx, &w.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "KMS client configured") -} - -func (w *wrappingKeyDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { - response.Schema = schema.Schema{ - Description: "KMS wrapping key datasource schema.", - Attributes: map[string]schema.Attribute{ - "access_scope": schema.StringAttribute{ - Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)), - Computed: true, - }, - "algorithm": schema.StringAttribute{ - Description: fmt.Sprintf("The wrapping algorithm used to wrap the key to import. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingAlgorithmEnumValues)...)), - Computed: true, - }, - "description": schema.StringAttribute{ - Description: "A user chosen description to distinguish multiple wrapping keys.", - Computed: true, - }, - "display_name": schema.StringAttribute{ - Description: "The display name to distinguish multiple wrapping keys.", - Computed: true, - }, - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`wrapping_key_id`\".", - Computed: true, - }, - "keyring_id": schema.StringAttribute{ - Description: "The ID of the associated keyring", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "protection": schema.StringAttribute{ - Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)), - Computed: true, - }, - "purpose": schema.StringAttribute{ - Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingPurposeEnumValues)...)), - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the keyring is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - "wrapping_key_id": schema.StringAttribute{ - Description: "The ID of the wrapping key", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "public_key": schema.StringAttribute{ - Description: "The public key of the wrapping key.", - Computed: true, - }, - "expires_at": schema.StringAttribute{ - Description: "The date and time the wrapping key will expire.", - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: "The date and time the creation of the wrapping key was triggered.", - Computed: true, - }, - }, - } -} - -func (w *wrappingKeyDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := request.Config.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := w.providerData.GetRegionWithOverride(model.Region) - wrappingKeyId := model.WrappingKeyId.ValueString() - - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "wrapping_key_id", wrappingKeyId) - - wrappingKeyResponse, err := w.client.GetWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() - if err != nil { - utils.LogError( - ctx, - &response.Diagnostics, - err, - "Reading wrapping key", - fmt.Sprintf("Wrapping key with ID %q does not exist in project %q.", wrappingKeyId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - response.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(wrappingKeyResponse, &model, region) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading wrapping key", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = response.State.Set(ctx, model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Wrapping key read") -} diff --git a/stackit/internal/services/kms/wrapping-key/resource.go b/stackit/internal/services/kms/wrapping-key/resource.go deleted file mode 100644 index d534b115..00000000 --- a/stackit/internal/services/kms/wrapping-key/resource.go +++ /dev/null @@ -1,467 +0,0 @@ -package kms - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" - "github.com/stackitcloud/stackit-sdk-go/services/kms/wait" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/kms" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - kmsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -var ( - _ resource.Resource = &wrappingKeyResource{} - _ resource.ResourceWithConfigure = &wrappingKeyResource{} - _ resource.ResourceWithImportState = &wrappingKeyResource{} - _ resource.ResourceWithModifyPlan = &wrappingKeyResource{} -) - -type Model struct { - AccessScope types.String `tfsdk:"access_scope"` - Algorithm types.String `tfsdk:"algorithm"` - Description types.String `tfsdk:"description"` - DisplayName types.String `tfsdk:"display_name"` - Id types.String `tfsdk:"id"` // needed by TF - KeyRingId types.String `tfsdk:"keyring_id"` - Protection types.String `tfsdk:"protection"` - Purpose types.String `tfsdk:"purpose"` - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - WrappingKeyId types.String `tfsdk:"wrapping_key_id"` - PublicKey types.String `tfsdk:"public_key"` - ExpiresAt types.String `tfsdk:"expires_at"` - CreatedAt types.String `tfsdk:"created_at"` -} - -func NewWrappingKeyResource() resource.Resource { - return &wrappingKeyResource{} -} - -type wrappingKeyResource struct { - client *kms.APIClient - providerData core.ProviderData -} - -func (r *wrappingKeyResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { - response.TypeName = request.ProviderTypeName + "_kms_wrapping_key" -} - -func (r *wrappingKeyResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - r.client = kmsUtils.ConfigureClient(ctx, &r.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "KMS client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *wrappingKeyResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (r *wrappingKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { - response.Schema = schema.Schema{ - Description: "KMS wrapping key resource schema.", - Attributes: map[string]schema.Attribute{ - "access_scope": schema.StringAttribute{ - Description: fmt.Sprintf("The access scope of the key. Default is `%s`. %s", string(kms.ACCESSSCOPE_PUBLIC), utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedAccessScopeEnumValues)...)), - Optional: true, - Computed: true, - Default: stringdefault.StaticString(string(kms.ACCESSSCOPE_PUBLIC)), - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "algorithm": schema.StringAttribute{ - Description: fmt.Sprintf("The wrapping algorithm used to wrap the key to import. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingAlgorithmEnumValues)...)), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "description": schema.StringAttribute{ - Description: "A user chosen description to distinguish multiple wrapping keys.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "display_name": schema.StringAttribute{ - Description: "The display name to distinguish multiple wrapping keys.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`keyring_id`,`wrapping_key_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "keyring_id": schema.StringAttribute{ - Description: "The ID of the associated keyring", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "protection": schema.StringAttribute{ - Description: fmt.Sprintf("The underlying system that is responsible for protecting the key material. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedProtectionEnumValues)...)), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "purpose": schema.StringAttribute{ - Description: fmt.Sprintf("The purpose for which the key will be used. %s", utils.FormatPossibleValues(sdkUtils.EnumSliceToStringSlice(kms.AllowedWrappingPurposeEnumValues)...)), - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the keyring is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "wrapping_key_id": schema.StringAttribute{ - Description: "The ID of the wrapping key", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "public_key": schema.StringAttribute{ - Description: "The public key of the wrapping key.", - Computed: true, - }, - "expires_at": schema.StringAttribute{ - Description: "The date and time the wrapping key will expire.", - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: "The date and time the creation of the wrapping key was triggered.", - Computed: true, - }, - }, - } -} - -func (r *wrappingKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - keyRingId := model.KeyRingId.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - createWrappingKeyResp, err := r.client.CreateWrappingKey(ctx, projectId, region, keyRingId).CreateWrappingKeyPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if createWrappingKeyResp == nil || createWrappingKeyResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", "API returned empty response") - return - } - - wrappingKeyId := *createWrappingKeyResp.Id - - // Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{ - "project_id": projectId, - "region": region, - "keyring_id": keyRingId, - "wrapping_key_id": wrappingKeyId, - }) - - wrappingKey, err := wait.CreateWrappingKeyWaitHandler(ctx, r.client, projectId, region, keyRingId, wrappingKeyId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error waiting for wrapping key creation", fmt.Sprintf("Calling API: %v", err)) - return - } - - err = mapFields(wrappingKey, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating wrapping key", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Key created") -} - -func (r *wrappingKeyResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := request.State.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - wrappingKeyId := model.WrappingKeyId.ValueString() - - ctx = tflog.SetField(ctx, "keyring_id", keyRingId) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "wrapping_key_id", wrappingKeyId) - - wrappingKeyResponse, err := r.client.GetWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - response.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading wrapping key", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(wrappingKeyResponse, &model, region) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading wrapping key", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = response.State.Set(ctx, model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Wrapping key read") -} - -func (r *wrappingKeyResource) Update(ctx context.Context, _ resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // wrapping keys cannot be updated, so we log an error. - core.LogAndAddError(ctx, &response.Diagnostics, "Error updating wrapping key", "Keys can't be updated") -} - -func (r *wrappingKeyResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := request.State.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - keyRingId := model.KeyRingId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - wrappingKeyId := model.WrappingKeyId.ValueString() - - err := r.client.DeleteWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting wrapping key", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "wrapping key deleted") -} - -func (r *wrappingKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing wrapping key", - fmt.Sprintf("Exptected import identifier with format: [project_id],[region],[keyring_id],[wrapping_key_id], got :%q", req.ID), - ) - return - } - - utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ - "project_id": idParts[0], - "region": idParts[1], - "keyring_id": idParts[2], - "wrapping_key_id": idParts[3], - }) - - tflog.Info(ctx, "wrapping key state imported") -} - -func mapFields(wrappingKey *kms.WrappingKey, model *Model, region string) error { - if wrappingKey == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var wrappingKeyId string - if model.WrappingKeyId.ValueString() != "" { - wrappingKeyId = model.WrappingKeyId.ValueString() - } else if wrappingKey.Id != nil { - wrappingKeyId = *wrappingKey.Id - } else { - return fmt.Errorf("key id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.KeyRingId.ValueString(), wrappingKeyId) - model.Region = types.StringValue(region) - model.WrappingKeyId = types.StringValue(wrappingKeyId) - model.DisplayName = types.StringPointerValue(wrappingKey.DisplayName) - model.PublicKey = types.StringPointerValue(wrappingKey.PublicKey) - model.AccessScope = types.StringValue(string(wrappingKey.GetAccessScope())) - model.Algorithm = types.StringValue(string(wrappingKey.GetAlgorithm())) - model.Purpose = types.StringValue(string(wrappingKey.GetPurpose())) - model.Protection = types.StringValue(string(wrappingKey.GetProtection())) - - model.CreatedAt = types.StringNull() - if wrappingKey.CreatedAt != nil { - model.CreatedAt = types.StringValue(wrappingKey.CreatedAt.Format(time.RFC3339)) - } - - model.ExpiresAt = types.StringNull() - if wrappingKey.ExpiresAt != nil { - model.ExpiresAt = types.StringValue(wrappingKey.ExpiresAt.Format(time.RFC3339)) - } - - // TODO: workaround - remove once STACKITKMS-377 is resolved (just write the return value from the API to the state then) - if !(model.Description.IsNull() && wrappingKey.Description != nil && *wrappingKey.Description == "") { - model.Description = types.StringPointerValue(wrappingKey.Description) - } else { - model.Description = types.StringNull() - } - - return nil -} - -func toCreatePayload(model *Model) (*kms.CreateWrappingKeyPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - return &kms.CreateWrappingKeyPayload{ - AccessScope: kms.CreateKeyPayloadGetAccessScopeAttributeType(conversion.StringValueToPointer(model.AccessScope)), - Algorithm: kms.CreateWrappingKeyPayloadGetAlgorithmAttributeType(conversion.StringValueToPointer(model.Algorithm)), - Description: conversion.StringValueToPointer(model.Description), - DisplayName: conversion.StringValueToPointer(model.DisplayName), - Protection: kms.CreateKeyPayloadGetProtectionAttributeType(conversion.StringValueToPointer(model.Protection)), - Purpose: kms.CreateWrappingKeyPayloadGetPurposeAttributeType(conversion.StringValueToPointer(model.Purpose)), - }, nil -} diff --git a/stackit/internal/services/kms/wrapping-key/resource_test.go b/stackit/internal/services/kms/wrapping-key/resource_test.go deleted file mode 100644 index fcc189e0..00000000 --- a/stackit/internal/services/kms/wrapping-key/resource_test.go +++ /dev/null @@ -1,244 +0,0 @@ -package kms - -import ( - "fmt" - "testing" - - "github.com/google/uuid" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/kms" -) - -var ( - projectId = uuid.NewString() - keyRingId = uuid.NewString() - wrappingKeyId = uuid.NewString() -) - -func TestMapFields(t *testing.T) { - type args struct { - state *Model - input *kms.WrappingKey - region string - } - tests := []struct { - description string - args args - expected *Model - isValid bool - }{ - { - description: "default values", - args: args{ - state: &Model{ - KeyRingId: types.StringValue(keyRingId), - ProjectId: types.StringValue(projectId), - WrappingKeyId: types.StringValue(wrappingKeyId), - }, - input: &kms.WrappingKey{ - Id: utils.Ptr("wid"), - AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), - Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), - Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), - Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), - }, - region: "eu01", - }, - expected: &Model{ - Description: types.StringNull(), - DisplayName: types.StringNull(), - KeyRingId: types.StringValue(keyRingId), - Id: types.StringValue(fmt.Sprintf("%s,eu01,%s,%s", projectId, keyRingId, wrappingKeyId)), - ProjectId: types.StringValue(projectId), - Region: types.StringValue("eu01"), - WrappingKeyId: types.StringValue(wrappingKeyId), - AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)), - Algorithm: types.StringValue(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), - Purpose: types.StringValue(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)), - Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), - }, - isValid: true, - }, - { - description: "values_ok", - args: args{ - state: &Model{ - KeyRingId: types.StringValue(keyRingId), - ProjectId: types.StringValue(projectId), - WrappingKeyId: types.StringValue(wrappingKeyId), - }, - input: &kms.WrappingKey{ - Description: utils.Ptr("descr"), - DisplayName: utils.Ptr("name"), - Id: utils.Ptr(wrappingKeyId), - AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), - Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), - Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), - Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), - }, - region: "eu02", - }, - expected: &Model{ - Description: types.StringValue("descr"), - DisplayName: types.StringValue("name"), - KeyRingId: types.StringValue(keyRingId), - Id: types.StringValue(fmt.Sprintf("%s,eu02,%s,%s", projectId, keyRingId, wrappingKeyId)), - ProjectId: types.StringValue(projectId), - Region: types.StringValue("eu02"), - WrappingKeyId: types.StringValue(wrappingKeyId), - AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)), - Algorithm: types.StringValue(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), - Purpose: types.StringValue(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)), - Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), - }, - isValid: true, - }, - { - description: "nil_response_field", - args: args{ - state: &Model{}, - input: &kms.WrappingKey{ - Id: nil, - }, - }, - expected: &Model{}, - isValid: false, - }, - { - description: "nil_response", - args: args{ - state: &Model{}, - input: nil, - }, - expected: &Model{}, - isValid: false, - }, - { - description: "no_resource_id", - args: args{ - state: &Model{ - Region: types.StringValue("eu01"), - ProjectId: types.StringValue("pid"), - }, - input: &kms.WrappingKey{}, - }, - expected: &Model{}, - isValid: false, - }, - { - // TODO: test for workaround - remove once STACKITKMS-377 is resolved - description: "empty description string", - args: args{ - state: &Model{ - KeyRingId: types.StringValue(keyRingId), - ProjectId: types.StringValue(projectId), - WrappingKeyId: types.StringValue(wrappingKeyId), - }, - input: &kms.WrappingKey{ - Description: utils.Ptr(""), - Id: utils.Ptr(wrappingKeyId), - AccessScope: utils.Ptr(kms.ACCESSSCOPE_PUBLIC), - Algorithm: utils.Ptr(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256), - Purpose: utils.Ptr(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY), - Protection: utils.Ptr(kms.PROTECTION_SOFTWARE), - }, - region: "eu02", - }, - expected: &Model{ - Description: types.StringNull(), - KeyRingId: types.StringValue(keyRingId), - Id: types.StringValue(fmt.Sprintf("%s,eu02,%s,%s", projectId, keyRingId, wrappingKeyId)), - ProjectId: types.StringValue(projectId), - Region: types.StringValue("eu02"), - WrappingKeyId: types.StringValue(wrappingKeyId), - AccessScope: types.StringValue(string(kms.ACCESSSCOPE_PUBLIC)), - Algorithm: types.StringValue(string(kms.WRAPPINGALGORITHM__2048_OAEP_SHA256)), - Purpose: types.StringValue(string(kms.WRAPPINGPURPOSE_ASYMMETRIC_KEY)), - Protection: types.StringValue(string(kms.PROTECTION_SOFTWARE)), - }, - isValid: true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(tt.args.input, tt.args.state, tt.args.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - - if tt.isValid { - diff := cmp.Diff(tt.args.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *kms.CreateWrappingKeyPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &kms.CreateWrappingKeyPayload{}, - true, - }, - { - "simple_values", - &Model{ - DisplayName: types.StringValue("name"), - }, - &kms.CreateWrappingKeyPayload{ - DisplayName: utils.Ptr("name"), - }, - true, - }, - { - "null_fields", - &Model{ - DisplayName: types.StringValue(""), - Description: types.StringValue(""), - }, - &kms.CreateWrappingKeyPayload{ - DisplayName: utils.Ptr(""), - Description: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go deleted file mode 100644 index 8a6884de..00000000 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ /dev/null @@ -1,430 +0,0 @@ -package loadbalancer - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &loadBalancerDataSource{} -) - -// NewLoadBalancerDataSource is a helper function to simplify the provider implementation. -func NewLoadBalancerDataSource() datasource.DataSource { - return &loadBalancerDataSource{} -} - -// loadBalancerDataSource is the data source implementation. -type loadBalancerDataSource struct { - client *loadbalancer.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *loadBalancerDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_loadbalancer" -} - -// Configure adds the provider configured client to the data source. -func (r *loadBalancerDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := loadbalancerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Load balancer client configured") -} - -// Schema defines the schema for the data source. -func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - servicePlanOptions := []string{"p10", "p50", "p250", "p750"} - - descriptions := map[string]string{ - "main": "Load Balancer data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", - "project_id": "STACKIT project ID to which the Load Balancer is associated.", - "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", - "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA (STACKIT Network area). When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", - "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT Network areas (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", - "listeners": "List of all listeners which will accept traffic. Limited to 20.", - "port": "Port number where we listen for traffic.", - "protocol": "Protocol is the highest network protocol we understand to load balance.", - "target_pool": "Reference target pool by target pool name.", - "name": "Load balancer name.", - "plan_id": "The service plan ID. If not defined, the default service plan is `p10`. " + utils.FormatPossibleValues(servicePlanOptions...), - "networks": "List of networks that listeners and targets reside in.", - "network_id": "Openstack network ID.", - "role": "The role defines how the load balancer is using the network.", - "observability": "We offer Load Balancer metrics observability via ARGUS or external solutions.", - "observability_logs": "Observability logs configuration.", - "observability_logs_credentials_ref": "Credentials reference for logs.", - "observability_logs_push_url": "The ARGUS/Loki remote write Push URL to ship the logs to.", - "observability_metrics": "Observability metrics configuration.", - "observability_metrics_credentials_ref": "Credentials reference for metrics.", - "observability_metrics_push_url": "The ARGUS/Prometheus remote write Push URL to ship the metrics to.", - "options": "Defines any optional functionality you want to have enabled on your load balancer.", - "acl": "Load Balancer is accessible only from an IP address in this range.", - "private_network_only": "If true, Load Balancer is accessible only via a private network IP address.", - "session_persistence": "Here you can setup various session persistence options, so far only \"`use_source_ip_address`\" is supported.", - "use_source_ip_address": "If true then all connections from one source IP address are redirected to the same target. This setting changes the load balancing algorithm to Maglev.", - "server_name_indicators": "A list of domain names to match in order to pass TLS traffic to the target pool in the current listener", - "server_name_indicators.name": "A domain name to match in order to pass TLS traffic to the target pool in the current listener", - "private_address": "Transient private Load Balancer IP address. It can change any time.", - "target_pools": "List of all target pools which will be used in the Load Balancer. Limited to 20.", - "healthy_threshold": "Healthy threshold of the health checking.", - "interval": "Interval duration of health checking in seconds.", - "interval_jitter": "Interval duration threshold of the health checking in seconds.", - "timeout": "Active health checking timeout duration in seconds.", - "unhealthy_threshold": "Unhealthy threshold of the health checking.", - "target_pools.name": "Target pool name.", - "target_port": "Identical port number where each target listens for traffic.", - "targets": "List of all targets which will be used in the pool. Limited to 1000.", - "targets.display_name": "Target display name", - "ip": "Target IP", - "region": "The resource region. If not defined, the provider region is used.", - "tcp_options": "Options that are specific to the TCP protocol.", - "tcp_options_idle_timeout": "Time after which an idle connection is closed. The default value is set to 5 minutes, and the maximum value is one hour.", - "udp_options": "Options that are specific to the UDP protocol.", - "udp_options_idle_timeout": "Time after which an idle session is closed. The default value is set to 1 minute, and the maximum value is 2 minutes.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "external_address": schema.StringAttribute{ - Description: descriptions["external_address"], - Computed: true, - }, - "disable_security_group_assignment": schema.BoolAttribute{ - Description: descriptions["disable_security_group_assignment"], - Computed: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "listeners": schema.ListNestedAttribute{ - Description: descriptions["listeners"], - Computed: true, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 20), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "display_name": schema.StringAttribute{ - Description: descriptions["listeners.display_name"], - Computed: true, - }, - "port": schema.Int64Attribute{ - Description: descriptions["port"], - Computed: true, - }, - "protocol": schema.StringAttribute{ - Description: descriptions["protocol"], - Computed: true, - }, - "server_name_indicators": schema.ListNestedAttribute{ - Description: descriptions["server_name_indicators"], - Optional: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: descriptions["server_name_indicators.name"], - Optional: true, - }, - }, - }, - }, - "target_pool": schema.StringAttribute{ - Description: descriptions["target_pool"], - Computed: true, - }, - "tcp": schema.SingleNestedAttribute{ - Description: descriptions["tcp_options"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "idle_timeout": schema.StringAttribute{ - Description: descriptions["tcp_options_idle_timeout"], - Computed: true, - }, - }, - }, - "udp": schema.SingleNestedAttribute{ - Description: descriptions["udp_options"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "idle_timeout": schema.StringAttribute{ - Description: descriptions["udp_options_idle_timeout"], - Computed: true, - }, - }, - }, - }, - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - validate.NoSeparator(), - }, - }, - "networks": schema.ListNestedAttribute{ - Description: descriptions["networks"], - Computed: true, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "network_id": schema.StringAttribute{ - Description: descriptions["network_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "role": schema.StringAttribute{ - Description: descriptions["role"], - Computed: true, - }, - }, - }, - }, - "options": schema.SingleNestedAttribute{ - Description: descriptions["options"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "acl": schema.SetAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Computed: true, - Validators: []validator.Set{ - setvalidator.ValueStringsAre( - validate.CIDR(), - ), - }, - }, - "private_network_only": schema.BoolAttribute{ - Description: descriptions["private_network_only"], - Computed: true, - }, - "observability": schema.SingleNestedAttribute{ - Description: descriptions["observability"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "logs": schema.SingleNestedAttribute{ - Description: descriptions["observability_logs"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "credentials_ref": schema.StringAttribute{ - Description: descriptions["observability_logs_credentials_ref"], - Computed: true, - }, - "push_url": schema.StringAttribute{ - Description: descriptions["observability_logs_credentials_ref"], - Computed: true, - }, - }, - }, - "metrics": schema.SingleNestedAttribute{ - Description: descriptions["observability_metrics"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "credentials_ref": schema.StringAttribute{ - Description: descriptions["observability_metrics_credentials_ref"], - Computed: true, - }, - "push_url": schema.StringAttribute{ - Description: descriptions["observability_metrics_credentials_ref"], - Computed: true, - }, - }, - }, - }, - }, - }, - }, - "private_address": schema.StringAttribute{ - Description: descriptions["private_address"], - Computed: true, - }, - "target_pools": schema.ListNestedAttribute{ - Description: descriptions["target_pools"], - Computed: true, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 20), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "active_health_check": schema.SingleNestedAttribute{ - Description: descriptions["active_health_check"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "healthy_threshold": schema.Int64Attribute{ - Description: descriptions["healthy_threshold"], - Computed: true, - }, - "interval": schema.StringAttribute{ - Description: descriptions["interval"], - Computed: true, - }, - "interval_jitter": schema.StringAttribute{ - Description: descriptions["interval_jitter"], - Computed: true, - }, - "timeout": schema.StringAttribute{ - Description: descriptions["timeout"], - Computed: true, - }, - "unhealthy_threshold": schema.Int64Attribute{ - Description: descriptions["unhealthy_threshold"], - Computed: true, - }, - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["target_pools.name"], - Computed: true, - }, - "target_port": schema.Int64Attribute{ - Description: descriptions["target_port"], - Computed: true, - }, - "session_persistence": schema.SingleNestedAttribute{ - Description: descriptions["session_persistence"], - Optional: true, - Computed: false, - Attributes: map[string]schema.Attribute{ - "use_source_ip_address": schema.BoolAttribute{ - Description: descriptions["use_source_ip_address"], - Optional: true, - Computed: false, - }, - }, - }, - "targets": schema.ListNestedAttribute{ - Description: descriptions["targets"], - Computed: true, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 1000), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "display_name": schema.StringAttribute{ - Description: descriptions["targets.display_name"], - Computed: true, - }, - "ip": schema.StringAttribute{ - Description: descriptions["ip"], - Computed: true, - }, - }, - }, - }, - }, - }, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - "security_group_id": schema.StringAttribute{ - Description: descriptions["security_group_id"], - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - name := model.Name.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "region", region) - - lbResp, err := r.client.GetLoadBalancer(ctx, projectId, region, name).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading load balancer", - fmt.Sprintf("Load balancer with name %q does not exist in project %q.", name, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, lbResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Load balancer read") -} diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go deleted file mode 100644 index f062fc4a..00000000 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ /dev/null @@ -1,1755 +0,0 @@ -package loadbalancer - -import ( - "context" - "fmt" - "net/http" - "strings" - "time" - - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "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/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &loadBalancerResource{} - _ resource.ResourceWithConfigure = &loadBalancerResource{} - _ resource.ResourceWithImportState = &loadBalancerResource{} - _ resource.ResourceWithModifyPlan = &loadBalancerResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - ExternalAddress types.String `tfsdk:"external_address"` - DisableSecurityGroupAssignment types.Bool `tfsdk:"disable_security_group_assignment"` - Listeners types.List `tfsdk:"listeners"` - Name types.String `tfsdk:"name"` - PlanId types.String `tfsdk:"plan_id"` - Networks types.List `tfsdk:"networks"` - Options types.Object `tfsdk:"options"` - PrivateAddress types.String `tfsdk:"private_address"` - TargetPools types.List `tfsdk:"target_pools"` - Region types.String `tfsdk:"region"` - SecurityGroupId types.String `tfsdk:"security_group_id"` -} - -// Struct corresponding to Model.Listeners[i] -type listener struct { - DisplayName types.String `tfsdk:"display_name"` - Port types.Int64 `tfsdk:"port"` - Protocol types.String `tfsdk:"protocol"` - ServerNameIndicators types.List `tfsdk:"server_name_indicators"` - TargetPool types.String `tfsdk:"target_pool"` - TCP types.Object `tfsdk:"tcp"` - UDP types.Object `tfsdk:"udp"` -} - -// Types corresponding to listener -var listenerTypes = map[string]attr.Type{ - "display_name": types.StringType, - "port": types.Int64Type, - "protocol": types.StringType, - "server_name_indicators": types.ListType{ElemType: types.ObjectType{AttrTypes: serverNameIndicatorTypes}}, - "target_pool": types.StringType, - "tcp": types.ObjectType{AttrTypes: tcpTypes}, - "udp": types.ObjectType{AttrTypes: udpTypes}, -} - -// Struct corresponding to listener.ServerNameIndicators[i] -type serverNameIndicator struct { - Name types.String `tfsdk:"name"` -} - -// Types corresponding to serverNameIndicator -var serverNameIndicatorTypes = map[string]attr.Type{ - "name": types.StringType, -} - -type tcp struct { - IdleTimeout types.String `tfsdk:"idle_timeout"` -} - -var tcpTypes = map[string]attr.Type{ - "idle_timeout": types.StringType, -} - -type udp struct { - IdleTimeout types.String `tfsdk:"idle_timeout"` -} - -var udpTypes = map[string]attr.Type{ - "idle_timeout": types.StringType, -} - -// Struct corresponding to Model.Networks[i] -type network struct { - NetworkId types.String `tfsdk:"network_id"` - Role types.String `tfsdk:"role"` -} - -// Types corresponding to network -var networkTypes = map[string]attr.Type{ - "network_id": types.StringType, - "role": types.StringType, -} - -// Struct corresponding to Model.Options -type options struct { - ACL types.Set `tfsdk:"acl"` - PrivateNetworkOnly types.Bool `tfsdk:"private_network_only"` - Observability types.Object `tfsdk:"observability"` -} - -// Types corresponding to options -var optionsTypes = map[string]attr.Type{ - "acl": types.SetType{ElemType: types.StringType}, - "private_network_only": types.BoolType, - "observability": types.ObjectType{AttrTypes: observabilityTypes}, -} - -type observability struct { - Logs types.Object `tfsdk:"logs"` - Metrics types.Object `tfsdk:"metrics"` -} - -var observabilityTypes = map[string]attr.Type{ - "logs": types.ObjectType{AttrTypes: observabilityOptionTypes}, - "metrics": types.ObjectType{AttrTypes: observabilityOptionTypes}, -} - -type observabilityOption struct { - CredentialsRef types.String `tfsdk:"credentials_ref"` - PushUrl types.String `tfsdk:"push_url"` -} - -var observabilityOptionTypes = map[string]attr.Type{ - "credentials_ref": types.StringType, - "push_url": types.StringType, -} - -// Struct corresponding to Model.TargetPools[i] -type targetPool struct { - ActiveHealthCheck types.Object `tfsdk:"active_health_check"` - Name types.String `tfsdk:"name"` - TargetPort types.Int64 `tfsdk:"target_port"` - Targets types.List `tfsdk:"targets"` - SessionPersistence types.Object `tfsdk:"session_persistence"` -} - -// Types corresponding to targetPool -var targetPoolTypes = map[string]attr.Type{ - "active_health_check": types.ObjectType{AttrTypes: activeHealthCheckTypes}, - "name": types.StringType, - "target_port": types.Int64Type, - "targets": types.ListType{ElemType: types.ObjectType{AttrTypes: targetTypes}}, - "session_persistence": types.ObjectType{AttrTypes: sessionPersistenceTypes}, -} - -// Struct corresponding to targetPool.ActiveHealthCheck -type activeHealthCheck struct { - HealthyThreshold types.Int64 `tfsdk:"healthy_threshold"` - Interval types.String `tfsdk:"interval"` - IntervalJitter types.String `tfsdk:"interval_jitter"` - Timeout types.String `tfsdk:"timeout"` - UnhealthyThreshold types.Int64 `tfsdk:"unhealthy_threshold"` -} - -// Types corresponding to activeHealthCheck -var activeHealthCheckTypes = map[string]attr.Type{ - "healthy_threshold": types.Int64Type, - "interval": types.StringType, - "interval_jitter": types.StringType, - "timeout": types.StringType, - "unhealthy_threshold": types.Int64Type, -} - -// Struct corresponding to targetPool.Targets[i] -type target struct { - DisplayName types.String `tfsdk:"display_name"` - Ip types.String `tfsdk:"ip"` -} - -// Types corresponding to target -var targetTypes = map[string]attr.Type{ - "display_name": types.StringType, - "ip": types.StringType, -} - -// Struct corresponding to targetPool.SessionPersistence -type sessionPersistence struct { - UseSourceIPAddress types.Bool `tfsdk:"use_source_ip_address"` -} - -// Types corresponding to SessionPersistence -var sessionPersistenceTypes = map[string]attr.Type{ - "use_source_ip_address": types.BoolType, -} - -// NewLoadBalancerResource is a helper function to simplify the provider implementation. -func NewLoadBalancerResource() resource.Resource { - return &loadBalancerResource{} -} - -// loadBalancerResource is the resource implementation. -type loadBalancerResource struct { - client *loadbalancer.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *loadBalancerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_loadbalancer" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *loadBalancerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// ConfigValidators validates the resource configuration -func (r *loadBalancerResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var model Model - resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - // validation is done in extracted func so it's easier to unit-test it - validateConfig(ctx, &resp.Diagnostics, &model) -} - -func validateConfig(ctx context.Context, diags *diag.Diagnostics, model *Model) { - externalAddressIsSet := !model.ExternalAddress.IsNull() - - lbOptions, err := toOptionsPayload(ctx, model) - if err != nil || lbOptions == nil { - // private_network_only is not set and external_address is not set - if !externalAddressIsSet { - core.LogAndAddError(ctx, diags, "Error configuring load balancer", fmt.Sprintf("You need to provide either the `options.private_network_only = true` or `external_address` field. %v", err)) - } - return - } - if lbOptions.PrivateNetworkOnly == nil || !*lbOptions.PrivateNetworkOnly { - // private_network_only is not set or false and external_address is not set - if !externalAddressIsSet { - core.LogAndAddError(ctx, diags, "Error configuring load balancer", "You need to provide either the `options.private_network_only = true` or `external_address` field.") - } - return - } - - // Both are set - if *lbOptions.PrivateNetworkOnly && externalAddressIsSet { - core.LogAndAddError(ctx, diags, "Error configuring load balancer", "You need to provide either the `options.private_network_only = true` or `external_address` field.") - } -} - -// Configure adds the provider configured client to the resource. -func (r *loadBalancerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := loadbalancerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Load Balancer client configured") -} - -// Schema defines the schema for the resource. -func (r *loadBalancerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - protocolOptions := []string{"PROTOCOL_UNSPECIFIED", "PROTOCOL_TCP", "PROTOCOL_UDP", "PROTOCOL_TCP_PROXY", "PROTOCOL_TLS_PASSTHROUGH"} - roleOptions := []string{"ROLE_UNSPECIFIED", "ROLE_LISTENERS_AND_TARGETS", "ROLE_LISTENERS", "ROLE_TARGETS"} - servicePlanOptions := []string{"p10", "p50", "p250", "p750"} - - descriptions := map[string]string{ - "main": "Load Balancer resource schema.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`name`\".", - "project_id": "STACKIT project ID to which the Load Balancer is associated.", - "external_address": "External Load Balancer IP address where this Load Balancer is exposed.", - "disable_security_group_assignment": "If set to true, this will disable the automatic assignment of a security group to the load balancer's targets. This option is primarily used to allow targets that are not within the load balancer's own network or SNA (STACKIT network area). When this is enabled, you are fully responsible for ensuring network connectivity to the targets, including managing all routing and security group rules manually. This setting cannot be changed after the load balancer is created.", - "listeners": "List of all listeners which will accept traffic. Limited to 20.", - "port": "Port number where we listen for traffic.", - "protocol": "Protocol is the highest network protocol we understand to load balance. " + utils.FormatPossibleValues(protocolOptions...), - "target_pool": "Reference target pool by target pool name.", - "name": "Load balancer name.", - "plan_id": "The service plan ID. If not defined, the default service plan is `p10`. " + utils.FormatPossibleValues(servicePlanOptions...), - "networks": "List of networks that listeners and targets reside in.", - "network_id": "Openstack network ID.", - "role": "The role defines how the load balancer is using the network. " + utils.FormatPossibleValues(roleOptions...), - "observability": "We offer Load Balancer metrics observability via ARGUS or external solutions. Not changeable after creation.", - "observability_logs": "Observability logs configuration. Not changeable after creation.", - "observability_logs_credentials_ref": "Credentials reference for logs. Not changeable after creation.", - "observability_logs_push_url": "The ARGUS/Loki remote write Push URL to ship the logs to. Not changeable after creation.", - "observability_metrics": "Observability metrics configuration. Not changeable after creation.", - "observability_metrics_credentials_ref": "Credentials reference for metrics. Not changeable after creation.", - "observability_metrics_push_url": "The ARGUS/Prometheus remote write Push URL to ship the metrics to. Not changeable after creation.", - "options": "Defines any optional functionality you want to have enabled on your load balancer.", - "acl": "Load Balancer is accessible only from an IP address in this range.", - "private_network_only": "If true, Load Balancer is accessible only via a private network IP address.", - "session_persistence": "Here you can setup various session persistence options, so far only \"`use_source_ip_address`\" is supported.", - "use_source_ip_address": "If true then all connections from one source IP address are redirected to the same target. This setting changes the load balancing algorithm to Maglev.", - "server_name_indicators": "A list of domain names to match in order to pass TLS traffic to the target pool in the current listener", - "server_name_indicators.name": "A domain name to match in order to pass TLS traffic to the target pool in the current listener", - "private_address": "Transient private Load Balancer IP address. It can change any time.", - "target_pools": "List of all target pools which will be used in the Load Balancer. Limited to 20.", - "healthy_threshold": "Healthy threshold of the health checking.", - "interval": "Interval duration of health checking in seconds.", - "interval_jitter": "Interval duration threshold of the health checking in seconds.", - "timeout": "Active health checking timeout duration in seconds.", - "unhealthy_threshold": "Unhealthy threshold of the health checking.", - "target_pools.name": "Target pool name.", - "target_port": "Identical port number where each target listens for traffic.", - "targets": "List of all targets which will be used in the pool. Limited to 1000.", - "targets.display_name": "Target display name", - "ip": "Target IP", - "region": "The resource region. If not defined, the provider region is used.", - "security_group_id": "The ID of the egress security group assigned to the Load Balancer's internal machines. This ID is essential for allowing traffic from the Load Balancer to targets in different networks or STACKIT network areas (SNA). To enable this, create a security group rule for your target VMs and set the `remote_security_group_id` of that rule to this value. This is typically used when `disable_security_group_assignment` is set to `true`.", - "tcp_options": "Options that are specific to the TCP protocol.", - "tcp_options_idle_timeout": "Time after which an idle connection is closed. The default value is set to 300 seconds, and the maximum value is 3600 seconds. The format is a duration and the unit must be seconds. Example: 30s", - "udp_options": "Options that are specific to the UDP protocol.", - "udp_options_idle_timeout": "Time after which an idle session is closed. The default value is set to 1 minute, and the maximum value is 2 minutes. The format is a duration and the unit must be seconds. Example: 30s", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - MarkdownDescription: ` -## Setting up supporting infrastructure` + "\n" + ` - -The example below creates the supporting infrastructure using the STACKIT Terraform provider, including the network, network interface, a public IP address and server resources. -`, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "external_address": schema.StringAttribute{ - Description: descriptions["external_address"], - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "disable_security_group_assignment": schema.BoolAttribute{ - Description: descriptions["disable_security_group_assignment"], - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - boolplanmodifier.UseStateForUnknown(), - }, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "listeners": schema.ListNestedAttribute{ - Description: descriptions["listeners"], - Required: true, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 20), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "display_name": schema.StringAttribute{ - Description: descriptions["listeners.display_name"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "port": schema.Int64Attribute{ - Description: descriptions["port"], - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - int64planmodifier.UseStateForUnknown(), - }, - }, - "protocol": schema.StringAttribute{ - Description: descriptions["protocol"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.OneOf(protocolOptions...), - }, - }, - "server_name_indicators": schema.ListNestedAttribute{ - Description: descriptions["server_name_indicators"], - Optional: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: descriptions["server_name_indicators.name"], - Optional: true, - }, - }, - }, - }, - "target_pool": schema.StringAttribute{ - Description: descriptions["target_pool"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "tcp": schema.SingleNestedAttribute{ - Description: descriptions["tcp_options"], - Optional: true, - Attributes: map[string]schema.Attribute{ - "idle_timeout": schema.StringAttribute{ - Description: descriptions["tcp_options_idle_timeout"], - Optional: true, - }, - }, - }, - "udp": schema.SingleNestedAttribute{ - Description: descriptions["udp_options"], - Optional: true, - Computed: false, - Attributes: map[string]schema.Attribute{ - "idle_timeout": schema.StringAttribute{ - Description: descriptions["udp_options_idle_timeout"], - Optional: true, - }, - }, - }, - }, - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - validate.NoSeparator(), - }, - }, - "networks": schema.ListNestedAttribute{ - Description: descriptions["networks"], - Required: true, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "network_id": schema.StringAttribute{ - Description: descriptions["network_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "role": schema.StringAttribute{ - Description: descriptions["role"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.OneOf(roleOptions...), - }, - }, - }, - }, - }, - "options": schema.SingleNestedAttribute{ - Description: descriptions["options"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.RequiresReplace(), - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "acl": schema.SetAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Set{ - setplanmodifier.RequiresReplace(), - setplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.Set{ - setvalidator.ValueStringsAre( - validate.CIDR(), - ), - }, - }, - "private_network_only": schema.BoolAttribute{ - Description: descriptions["private_network_only"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - boolplanmodifier.UseStateForUnknown(), - }, - }, - "observability": schema.SingleNestedAttribute{ - Description: descriptions["observability"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - // API docs says observability options are not changeable after creation - objectplanmodifier.RequiresReplace(), - }, - Attributes: map[string]schema.Attribute{ - "logs": schema.SingleNestedAttribute{ - Description: descriptions["observability_logs"], - Optional: true, - Computed: true, - Attributes: map[string]schema.Attribute{ - "credentials_ref": schema.StringAttribute{ - Description: descriptions["observability_logs_credentials_ref"], - Optional: true, - Computed: true, - }, - "push_url": schema.StringAttribute{ - Description: descriptions["observability_logs_credentials_ref"], - Optional: true, - Computed: true, - }, - }, - }, - "metrics": schema.SingleNestedAttribute{ - Description: descriptions["observability_metrics"], - Optional: true, - Computed: true, - Attributes: map[string]schema.Attribute{ - "credentials_ref": schema.StringAttribute{ - Description: descriptions["observability_metrics_credentials_ref"], - Optional: true, - Computed: true, - }, - "push_url": schema.StringAttribute{ - Description: descriptions["observability_metrics_credentials_ref"], - Optional: true, - Computed: true, - }, - }, - }, - }, - }, - }, - }, - "private_address": schema.StringAttribute{ - Description: descriptions["private_address"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "target_pools": schema.ListNestedAttribute{ - Description: descriptions["target_pools"], - Required: true, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 20), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "active_health_check": schema.SingleNestedAttribute{ - Description: descriptions["active_health_check"], - Optional: true, - Computed: true, - Attributes: map[string]schema.Attribute{ - "healthy_threshold": schema.Int64Attribute{ - Description: descriptions["healthy_threshold"], - Optional: true, - Computed: true, - }, - "interval": schema.StringAttribute{ - Description: descriptions["interval"], - Optional: true, - Computed: true, - }, - "interval_jitter": schema.StringAttribute{ - Description: descriptions["interval_jitter"], - Optional: true, - Computed: true, - }, - "timeout": schema.StringAttribute{ - Description: descriptions["timeout"], - Optional: true, - Computed: true, - }, - "unhealthy_threshold": schema.Int64Attribute{ - Description: descriptions["unhealthy_threshold"], - Optional: true, - Computed: true, - }, - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["target_pools.name"], - Required: true, - }, - "target_port": schema.Int64Attribute{ - Description: descriptions["target_port"], - Required: true, - }, - "session_persistence": schema.SingleNestedAttribute{ - Description: descriptions["session_persistence"], - Optional: true, - Computed: false, - Attributes: map[string]schema.Attribute{ - "use_source_ip_address": schema.BoolAttribute{ - Description: descriptions["use_source_ip_address"], - Optional: true, - Computed: false, - }, - }, - }, - "targets": schema.ListNestedAttribute{ - Description: descriptions["targets"], - Required: true, - Validators: []validator.List{ - listvalidator.SizeBetween(1, 1000), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "display_name": schema.StringAttribute{ - Description: descriptions["targets.display_name"], - Required: true, - }, - "ip": schema.StringAttribute{ - Description: descriptions["ip"], - Required: true, - }, - }, - }, - }, - }, - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "security_group_id": schema.StringAttribute{ - Description: descriptions["security_group_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create a new load balancer - createResp, err := r.client.CreateLoadBalancer(ctx, projectId, region).CreateLoadBalancerPayload(*payload).XRequestID(uuid.NewString()).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.CreateLoadBalancerWaitHandler(ctx, r.client, projectId, region, *createResp.Name).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Load balancer creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Load balancer created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - name := model.Name.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "region", region) - - lbResp, err := r.client.GetLoadBalancer(ctx, projectId, region, name).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, lbResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Load balancer read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - name := model.Name.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "region", region) - - targetPoolsModel := []targetPool{} - diags = model.TargetPools.ElementsAs(ctx, &targetPoolsModel, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - for i := range targetPoolsModel { - targetPoolModel := targetPoolsModel[i] - targetPoolName := targetPoolModel.Name.ValueString() - ctx = tflog.SetField(ctx, "target_pool_name", targetPoolName) - - // Generate API request body from model - payload, err := toTargetPoolUpdatePayload(ctx, sdkUtils.Ptr(targetPoolModel)) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Creating API payload for target pool: %v", err)) - return - } - - // Update target pool - _, err = r.client.UpdateTargetPool(ctx, projectId, region, name, targetPoolName).UpdateTargetPoolPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Calling API for target pool: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - } - ctx = tflog.SetField(ctx, "target_pool_name", nil) - - // Get updated load balancer - getResp, err := r.client.GetLoadBalancer(ctx, projectId, region, name).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating load balancer", fmt.Sprintf("Calling API after update: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, getResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating load balancer", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Load balancer updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *loadBalancerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - name := model.Name.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "region", region) - - // Delete load balancer - _, err := r.client.DeleteLoadBalancer(ctx, projectId, region, name).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting load balancer", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteLoadBalancerWaitHandler(ctx, r.client, projectId, region, name).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting load balancer", fmt.Sprintf("Load balancer deleting waiting: %v", err)) - return - } - - tflog.Info(ctx, "Load balancer deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name -func (r *loadBalancerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing load balancer", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[name] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "Load balancer state imported") -} - -// toCreatePayload and all other toX functions in this file turn a Terraform load balancer model into a createLoadBalancerPayload to be used with the load balancer API. -func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoadBalancerPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - listenersPayload, err := toListenersPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting listeners: %w", err) - } - networksPayload, err := toNetworksPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting networks: %w", err) - } - optionsPayload, err := toOptionsPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting options: %w", err) - } - targetPoolsPayload, err := toTargetPoolsPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("converting target_pools: %w", err) - } - - return &loadbalancer.CreateLoadBalancerPayload{ - ExternalAddress: conversion.StringValueToPointer(model.ExternalAddress), - DisableTargetSecurityGroupAssignment: conversion.BoolValueToPointer(model.DisableSecurityGroupAssignment), - Listeners: listenersPayload, - Name: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - Networks: networksPayload, - Options: optionsPayload, - TargetPools: targetPoolsPayload, - }, nil -} - -func toListenersPayload(ctx context.Context, model *Model) (*[]loadbalancer.Listener, error) { - if model.Listeners.IsNull() || model.Listeners.IsUnknown() { - return nil, nil - } - - listenersModel := []listener{} - diags := model.Listeners.ElementsAs(ctx, &listenersModel, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - if len(listenersModel) == 0 { - return nil, nil - } - - payload := []loadbalancer.Listener{} - for i := range listenersModel { - listenerModel := listenersModel[i] - serverNameIndicatorsPayload, err := toServerNameIndicatorsPayload(ctx, &listenerModel) - if err != nil { - return nil, fmt.Errorf("converting index %d: converting server_name_indicator: %w", i, err) - } - tcp, err := toTCP(ctx, &listenerModel) - if err != nil { - return nil, fmt.Errorf("converting index %d: converting tcp: %w", i, err) - } - udp, err := toUDP(ctx, &listenerModel) - if err != nil { - return nil, fmt.Errorf("converting index %d: converting udp: %w", i, err) - } - payload = append(payload, loadbalancer.Listener{ - DisplayName: conversion.StringValueToPointer(listenerModel.DisplayName), - Port: conversion.Int64ValueToPointer(listenerModel.Port), - Protocol: loadbalancer.ListenerGetProtocolAttributeType(conversion.StringValueToPointer(listenerModel.Protocol)), - ServerNameIndicators: serverNameIndicatorsPayload, - TargetPool: conversion.StringValueToPointer(listenerModel.TargetPool), - Tcp: tcp, - Udp: udp, - }) - } - - return &payload, nil -} - -func toServerNameIndicatorsPayload(ctx context.Context, l *listener) (*[]loadbalancer.ServerNameIndicator, error) { - if l.ServerNameIndicators.IsNull() || l.ServerNameIndicators.IsUnknown() { - return nil, nil - } - - serverNameIndicatorsModel := []serverNameIndicator{} - diags := l.ServerNameIndicators.ElementsAs(ctx, &serverNameIndicatorsModel, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - payload := []loadbalancer.ServerNameIndicator{} - for i := range serverNameIndicatorsModel { - indicatorModel := serverNameIndicatorsModel[i] - payload = append(payload, loadbalancer.ServerNameIndicator{ - Name: conversion.StringValueToPointer(indicatorModel.Name), - }) - } - - return &payload, nil -} - -func toTCP(ctx context.Context, listener *listener) (*loadbalancer.OptionsTCP, error) { - if listener.TCP.IsNull() || listener.TCP.IsUnknown() { - return nil, nil - } - - tcp := tcp{} - diags := listener.TCP.As(ctx, &tcp, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - if tcp.IdleTimeout.IsNull() || tcp.IdleTimeout.IsUnknown() { - return nil, nil - } - - return &loadbalancer.OptionsTCP{ - IdleTimeout: tcp.IdleTimeout.ValueStringPointer(), - }, nil -} - -func toUDP(ctx context.Context, listener *listener) (*loadbalancer.OptionsUDP, error) { - if listener.UDP.IsNull() || listener.UDP.IsUnknown() { - return nil, nil - } - - udp := udp{} - diags := listener.UDP.As(ctx, &udp, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - if udp.IdleTimeout.IsNull() || udp.IdleTimeout.IsUnknown() { - return nil, nil - } - - return &loadbalancer.OptionsUDP{ - IdleTimeout: udp.IdleTimeout.ValueStringPointer(), - }, nil -} - -func toNetworksPayload(ctx context.Context, model *Model) (*[]loadbalancer.Network, error) { - if model.Networks.IsNull() || model.Networks.IsUnknown() { - return nil, nil - } - - networksModel := []network{} - diags := model.Networks.ElementsAs(ctx, &networksModel, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - if len(networksModel) == 0 { - return nil, nil - } - - payload := []loadbalancer.Network{} - for i := range networksModel { - networkModel := networksModel[i] - payload = append(payload, loadbalancer.Network{ - NetworkId: conversion.StringValueToPointer(networkModel.NetworkId), - Role: loadbalancer.NetworkGetRoleAttributeType(conversion.StringValueToPointer(networkModel.Role)), - }) - } - - return &payload, nil -} - -func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBalancerOptions, error) { - if model.Options.IsNull() || model.Options.IsUnknown() { - return &loadbalancer.LoadBalancerOptions{ - AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{}, - Observability: &loadbalancer.LoadbalancerOptionObservability{}, - }, nil - } - - optionsModel := options{} - diags := model.Options.As(ctx, &optionsModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - accessControlPayload := &loadbalancer.LoadbalancerOptionAccessControl{} - if !(optionsModel.ACL.IsNull() || optionsModel.ACL.IsUnknown()) { - var aclModel []string - diags := optionsModel.ACL.ElementsAs(ctx, &aclModel, false) - if diags.HasError() { - return nil, fmt.Errorf("converting acl: %w", core.DiagsToError(diags)) - } - accessControlPayload.AllowedSourceRanges = &aclModel - } - - observabilityPayload := &loadbalancer.LoadbalancerOptionObservability{} - if !(optionsModel.Observability.IsNull() || optionsModel.Observability.IsUnknown()) { - observabilityModel := observability{} - diags := optionsModel.Observability.As(ctx, &observabilityModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting observability: %w", core.DiagsToError(diags)) - } - - // observability logs - observabilityLogsModel := observabilityOption{} - diags = observabilityModel.Logs.As(ctx, &observabilityLogsModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting observability logs: %w", core.DiagsToError(diags)) - } - observabilityPayload.Logs = &loadbalancer.LoadbalancerOptionLogs{ - CredentialsRef: observabilityLogsModel.CredentialsRef.ValueStringPointer(), - PushUrl: observabilityLogsModel.PushUrl.ValueStringPointer(), - } - - // observability metrics - observabilityMetricsModel := observabilityOption{} - diags = observabilityModel.Metrics.As(ctx, &observabilityMetricsModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting observability metrics: %w", core.DiagsToError(diags)) - } - observabilityPayload.Metrics = &loadbalancer.LoadbalancerOptionMetrics{ - CredentialsRef: observabilityMetricsModel.CredentialsRef.ValueStringPointer(), - PushUrl: observabilityMetricsModel.PushUrl.ValueStringPointer(), - } - } - - payload := loadbalancer.LoadBalancerOptions{ - AccessControl: accessControlPayload, - Observability: observabilityPayload, - PrivateNetworkOnly: conversion.BoolValueToPointer(optionsModel.PrivateNetworkOnly), - } - - return &payload, nil -} - -func toTargetPoolsPayload(ctx context.Context, model *Model) (*[]loadbalancer.TargetPool, error) { - if model.TargetPools.IsNull() || model.TargetPools.IsUnknown() { - return nil, nil - } - - targetPoolsModel := []targetPool{} - diags := model.TargetPools.ElementsAs(ctx, &targetPoolsModel, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - if len(targetPoolsModel) == 0 { - return nil, nil - } - - payload := []loadbalancer.TargetPool{} - for i := range targetPoolsModel { - targetPoolModel := targetPoolsModel[i] - - activeHealthCheckPayload, err := toActiveHealthCheckPayload(ctx, &targetPoolModel) - if err != nil { - return nil, fmt.Errorf("converting index %d: converting active_health_check: %w", i, err) - } - sessionPersistencePayload, err := toSessionPersistencePayload(ctx, &targetPoolModel) - if err != nil { - return nil, fmt.Errorf("converting index %d: converting session_persistence: %w", i, err) - } - targetsPayload, err := toTargetsPayload(ctx, &targetPoolModel) - if err != nil { - return nil, fmt.Errorf("converting index %d: converting targets: %w", i, err) - } - - payload = append(payload, loadbalancer.TargetPool{ - ActiveHealthCheck: activeHealthCheckPayload, - Name: conversion.StringValueToPointer(targetPoolModel.Name), - SessionPersistence: sessionPersistencePayload, - TargetPort: conversion.Int64ValueToPointer(targetPoolModel.TargetPort), - Targets: targetsPayload, - }) - } - - return &payload, nil -} - -func toTargetPoolUpdatePayload(ctx context.Context, tp *targetPool) (*loadbalancer.UpdateTargetPoolPayload, error) { - if tp == nil { - return nil, fmt.Errorf("nil target pool") - } - - activeHealthCheckPayload, err := toActiveHealthCheckPayload(ctx, tp) - if err != nil { - return nil, fmt.Errorf("converting active_health_check: %w", err) - } - sessionPersistencePayload, err := toSessionPersistencePayload(ctx, tp) - if err != nil { - return nil, fmt.Errorf("converting session_persistence: %w", err) - } - targetsPayload, err := toTargetsPayload(ctx, tp) - if err != nil { - return nil, fmt.Errorf("converting targets: %w", err) - } - - return &loadbalancer.UpdateTargetPoolPayload{ - ActiveHealthCheck: activeHealthCheckPayload, - Name: conversion.StringValueToPointer(tp.Name), - SessionPersistence: sessionPersistencePayload, - TargetPort: conversion.Int64ValueToPointer(tp.TargetPort), - Targets: targetsPayload, - }, nil -} - -func toSessionPersistencePayload(ctx context.Context, tp *targetPool) (*loadbalancer.SessionPersistence, error) { - if tp.SessionPersistence.IsNull() || tp.ActiveHealthCheck.IsUnknown() { - return nil, nil - } - - sessionPersistenceModel := sessionPersistence{} - diags := tp.SessionPersistence.As(ctx, &sessionPersistenceModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - return &loadbalancer.SessionPersistence{ - UseSourceIpAddress: conversion.BoolValueToPointer(sessionPersistenceModel.UseSourceIPAddress), - }, nil -} - -func toActiveHealthCheckPayload(ctx context.Context, tp *targetPool) (*loadbalancer.ActiveHealthCheck, error) { - if tp.ActiveHealthCheck.IsNull() || tp.ActiveHealthCheck.IsUnknown() { - return nil, nil - } - - activeHealthCheckModel := activeHealthCheck{} - diags := tp.ActiveHealthCheck.As(ctx, &activeHealthCheckModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting active health check: %w", core.DiagsToError(diags)) - } - - return &loadbalancer.ActiveHealthCheck{ - HealthyThreshold: conversion.Int64ValueToPointer(activeHealthCheckModel.HealthyThreshold), - Interval: conversion.StringValueToPointer(activeHealthCheckModel.Interval), - IntervalJitter: conversion.StringValueToPointer(activeHealthCheckModel.IntervalJitter), - Timeout: conversion.StringValueToPointer(activeHealthCheckModel.Timeout), - UnhealthyThreshold: conversion.Int64ValueToPointer(activeHealthCheckModel.UnhealthyThreshold), - }, nil -} - -func toTargetsPayload(ctx context.Context, tp *targetPool) (*[]loadbalancer.Target, error) { - if tp.Targets.IsNull() || tp.Targets.IsUnknown() { - return nil, nil - } - - targetsModel := []target{} - diags := tp.Targets.ElementsAs(ctx, &targetsModel, false) - if diags.HasError() { - return nil, fmt.Errorf("converting Targets list: %w", core.DiagsToError(diags)) - } - - if len(targetsModel) == 0 { - return nil, nil - } - - payload := []loadbalancer.Target{} - for i := range targetsModel { - targetModel := targetsModel[i] - payload = append(payload, loadbalancer.Target{ - DisplayName: conversion.StringValueToPointer(targetModel.DisplayName), - Ip: conversion.StringValueToPointer(targetModel.Ip), - }) - } - - return &payload, nil -} - -// mapFields and all other map functions in this file translate an API resource into a Terraform model. -func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, region string) error { - if lb == nil { - return fmt.Errorf("response input is nil") - } - if m == nil { - return fmt.Errorf("model input is nil") - } - - var name string - if m.Name.ValueString() != "" { - name = m.Name.ValueString() - } else if lb.Name != nil { - name = *lb.Name - } else { - return fmt.Errorf("name not present") - } - m.Region = types.StringValue(region) - m.Name = types.StringValue(name) - m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), name) - - m.PlanId = types.StringPointerValue(lb.PlanId) - m.ExternalAddress = types.StringPointerValue(lb.ExternalAddress) - m.PrivateAddress = types.StringPointerValue(lb.PrivateAddress) - m.DisableSecurityGroupAssignment = types.BoolPointerValue(lb.DisableTargetSecurityGroupAssignment) - - if lb.TargetSecurityGroup != nil { - m.SecurityGroupId = types.StringPointerValue(lb.TargetSecurityGroup.Id) - } else { - m.SecurityGroupId = types.StringNull() - } - err := mapListeners(lb, m) - if err != nil { - return fmt.Errorf("mapping listeners: %w", err) - } - err = mapNetworks(lb, m) - if err != nil { - return fmt.Errorf("mapping network: %w", err) - } - err = mapOptions(ctx, lb, m) - if err != nil { - return fmt.Errorf("mapping options: %w", err) - } - err = mapTargetPools(lb, m) - if err != nil { - return fmt.Errorf("mapping target pools: %w", err) - } - - return nil -} - -func mapListeners(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { - if loadBalancerResp.Listeners == nil { - m.Listeners = types.ListNull(types.ObjectType{AttrTypes: listenerTypes}) - return nil - } - - listenersList := []attr.Value{} - for i, listenerResp := range *loadBalancerResp.Listeners { - listenerMap := map[string]attr.Value{ - "display_name": types.StringPointerValue(listenerResp.DisplayName), - "port": types.Int64PointerValue(listenerResp.Port), - "protocol": types.StringValue(string(listenerResp.GetProtocol())), - "target_pool": types.StringPointerValue(listenerResp.TargetPool), - } - - err := mapServerNameIndicators(listenerResp.ServerNameIndicators, listenerMap) - if err != nil { - return fmt.Errorf("mapping index %d, field serverNameIndicators: %w", i, err) - } - - err = mapTCP(listenerResp.Tcp, listenerMap) - if err != nil { - return fmt.Errorf("mapping index %d, field tcp: %w", i, err) - } - - err = mapUDP(listenerResp.Udp, listenerMap) - if err != nil { - return fmt.Errorf("mapping index %d, field udp: %w", i, err) - } - - listenerTF, diags := types.ObjectValue(listenerTypes, listenerMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - - listenersList = append(listenersList, listenerTF) - } - - listenersTF, diags := types.ListValue( - types.ObjectType{AttrTypes: listenerTypes}, - listenersList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - m.Listeners = listenersTF - return nil -} - -func mapServerNameIndicators(serverNameIndicatorsResp *[]loadbalancer.ServerNameIndicator, l map[string]attr.Value) error { - if serverNameIndicatorsResp == nil || *serverNameIndicatorsResp == nil { - l["server_name_indicators"] = types.ListNull(types.ObjectType{AttrTypes: serverNameIndicatorTypes}) - return nil - } - - serverNameIndicatorsList := []attr.Value{} - for i, serverNameIndicatorResp := range *serverNameIndicatorsResp { - serverNameIndicatorMap := map[string]attr.Value{ - "name": types.StringPointerValue(serverNameIndicatorResp.Name), - } - - serverNameIndicatorTF, diags := types.ObjectValue(serverNameIndicatorTypes, serverNameIndicatorMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - - serverNameIndicatorsList = append(serverNameIndicatorsList, serverNameIndicatorTF) - } - - serverNameIndicatorsTF, diags := types.ListValue( - types.ObjectType{AttrTypes: serverNameIndicatorTypes}, - serverNameIndicatorsList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - l["server_name_indicators"] = serverNameIndicatorsTF - return nil -} - -func mapTCP(tcp *loadbalancer.OptionsTCP, listener map[string]attr.Value) error { - if tcp == nil || tcp.IdleTimeout == nil || *tcp.IdleTimeout == "" { - listener["tcp"] = types.ObjectNull(tcpTypes) - return nil - } - - tcpAttr, diags := types.ObjectValue(tcpTypes, map[string]attr.Value{ - "idle_timeout": types.StringValue(*tcp.IdleTimeout), - }) - if diags.HasError() { - return core.DiagsToError(diags) - } - - listener["tcp"] = tcpAttr - return nil -} - -func mapUDP(udp *loadbalancer.OptionsUDP, listener map[string]attr.Value) error { - if udp == nil || udp.IdleTimeout == nil || *udp.IdleTimeout == "" { - listener["udp"] = types.ObjectNull(udpTypes) - return nil - } - - udpAttr, diags := types.ObjectValue(udpTypes, map[string]attr.Value{ - "idle_timeout": types.StringValue(*udp.IdleTimeout), - }) - if diags.HasError() { - return core.DiagsToError(diags) - } - - listener["udp"] = udpAttr - return nil -} - -func mapNetworks(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { - if loadBalancerResp.Networks == nil { - m.Networks = types.ListNull(types.ObjectType{AttrTypes: networkTypes}) - return nil - } - - networksList := []attr.Value{} - for i, networkResp := range *loadBalancerResp.Networks { - networkMap := map[string]attr.Value{ - "network_id": types.StringPointerValue(networkResp.NetworkId), - "role": types.StringValue(string(networkResp.GetRole())), - } - - networkTF, diags := types.ObjectValue(networkTypes, networkMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - - networksList = append(networksList, networkTF) - } - - networksTF, diags := types.ListValue( - types.ObjectType{AttrTypes: networkTypes}, - networksList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - m.Networks = networksTF - return nil -} - -func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { - if loadBalancerResp.Options == nil { - m.Options = types.ObjectNull(optionsTypes) - return nil - } - - privateNetworkOnlyTF := types.BoolPointerValue(loadBalancerResp.Options.PrivateNetworkOnly) - - // If the private_network_only field is nil in the response but is explicitly set to false in the model, - // we set it to false in the TF state to prevent an inconsistent result after apply error - if !m.Options.IsNull() && !m.Options.IsUnknown() { - optionsModel := options{} - diags := m.Options.As(ctx, &optionsModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return fmt.Errorf("convert options: %w", core.DiagsToError(diags)) - } - if loadBalancerResp.Options.PrivateNetworkOnly == nil && !optionsModel.PrivateNetworkOnly.IsNull() && !optionsModel.PrivateNetworkOnly.IsUnknown() && !optionsModel.PrivateNetworkOnly.ValueBool() { - privateNetworkOnlyTF = types.BoolValue(false) - } - } - - optionsMap := map[string]attr.Value{ - "private_network_only": privateNetworkOnlyTF, - } - - err := mapACL(loadBalancerResp.Options.AccessControl, optionsMap) - if err != nil { - return fmt.Errorf("mapping field ACL: %w", err) - } - - observabilityLogsMap := map[string]attr.Value{ - "credentials_ref": types.StringNull(), - "push_url": types.StringNull(), - } - if loadBalancerResp.Options.HasObservability() && loadBalancerResp.Options.Observability.HasLogs() { - observabilityLogsMap["credentials_ref"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Logs.CredentialsRef) - observabilityLogsMap["push_url"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Logs.PushUrl) - } - observabilityLogsTF, diags := types.ObjectValue(observabilityOptionTypes, observabilityLogsMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - - observabilityMetricsMap := map[string]attr.Value{ - "credentials_ref": types.StringNull(), - "push_url": types.StringNull(), - } - if loadBalancerResp.Options.HasObservability() && loadBalancerResp.Options.Observability.HasMetrics() { - observabilityMetricsMap["credentials_ref"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Metrics.CredentialsRef) - observabilityMetricsMap["push_url"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Metrics.PushUrl) - } - observabilityMetricsTF, diags := types.ObjectValue(observabilityOptionTypes, observabilityMetricsMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - - observabilityMap := map[string]attr.Value{ - "logs": observabilityLogsTF, - "metrics": observabilityMetricsTF, - } - observabilityTF, diags := types.ObjectValue(observabilityTypes, observabilityMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - optionsMap["observability"] = observabilityTF - - optionsTF, diags := types.ObjectValue(optionsTypes, optionsMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - - m.Options = optionsTF - return nil -} - -func mapACL(accessControlResp *loadbalancer.LoadbalancerOptionAccessControl, o map[string]attr.Value) error { - if accessControlResp == nil || accessControlResp.AllowedSourceRanges == nil { - o["acl"] = types.SetNull(types.StringType) - return nil - } - - aclList := []attr.Value{} - for _, rangeResp := range *accessControlResp.AllowedSourceRanges { - rangeTF := types.StringValue(rangeResp) - aclList = append(aclList, rangeTF) - } - - aclTF, diags := types.SetValue(types.StringType, aclList) - if diags.HasError() { - return core.DiagsToError(diags) - } - - o["acl"] = aclTF - return nil -} - -func mapTargetPools(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { - if loadBalancerResp.TargetPools == nil { - m.TargetPools = types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}) - return nil - } - - targetPoolsList := []attr.Value{} - for i, targetPoolResp := range *loadBalancerResp.TargetPools { - targetPoolMap := map[string]attr.Value{ - "name": types.StringPointerValue(targetPoolResp.Name), - "target_port": types.Int64PointerValue(targetPoolResp.TargetPort), - } - - err := mapActiveHealthCheck(targetPoolResp.ActiveHealthCheck, targetPoolMap) - if err != nil { - return fmt.Errorf("mapping index %d, field ActiveHealthCheck: %w", i, err) - } - - err = mapTargets(targetPoolResp.Targets, targetPoolMap) - if err != nil { - return fmt.Errorf("mapping index %d, field Targets: %w", i, err) - } - - err = mapSessionPersistence(targetPoolResp.SessionPersistence, targetPoolMap) - if err != nil { - return fmt.Errorf("mapping index %d, field SessionPersistence: %w", i, err) - } - - targetPoolTF, diags := types.ObjectValue(targetPoolTypes, targetPoolMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - - targetPoolsList = append(targetPoolsList, targetPoolTF) - } - - targetPoolsTF, diags := types.ListValue( - types.ObjectType{AttrTypes: targetPoolTypes}, - targetPoolsList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - m.TargetPools = targetPoolsTF - return nil -} - -func mapActiveHealthCheck(activeHealthCheckResp *loadbalancer.ActiveHealthCheck, tp map[string]attr.Value) error { - if activeHealthCheckResp == nil { - tp["active_health_check"] = types.ObjectNull(activeHealthCheckTypes) - return nil - } - - activeHealthCheckMap := map[string]attr.Value{ - "healthy_threshold": types.Int64PointerValue(activeHealthCheckResp.HealthyThreshold), - "interval": types.StringPointerValue(activeHealthCheckResp.Interval), - "interval_jitter": types.StringPointerValue(activeHealthCheckResp.IntervalJitter), - "timeout": types.StringPointerValue(activeHealthCheckResp.Timeout), - "unhealthy_threshold": types.Int64PointerValue(activeHealthCheckResp.UnhealthyThreshold), - } - - activeHealthCheckTF, diags := types.ObjectValue(activeHealthCheckTypes, activeHealthCheckMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - - tp["active_health_check"] = activeHealthCheckTF - return nil -} - -func mapTargets(targetsResp *[]loadbalancer.Target, tp map[string]attr.Value) error { - if targetsResp == nil || *targetsResp == nil { - tp["targets"] = types.ListNull(types.ObjectType{AttrTypes: targetTypes}) - return nil - } - - targetsList := []attr.Value{} - for i, targetResp := range *targetsResp { - targetMap := map[string]attr.Value{ - "display_name": types.StringPointerValue(targetResp.DisplayName), - "ip": types.StringPointerValue(targetResp.Ip), - } - - targetTF, diags := types.ObjectValue(targetTypes, targetMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - - targetsList = append(targetsList, targetTF) - } - - targetsTF, diags := types.ListValue( - types.ObjectType{AttrTypes: targetTypes}, - targetsList, - ) - if diags.HasError() { - return core.DiagsToError(diags) - } - - tp["targets"] = targetsTF - return nil -} - -func mapSessionPersistence(sessionPersistenceResp *loadbalancer.SessionPersistence, tp map[string]attr.Value) error { - if sessionPersistenceResp == nil { - tp["session_persistence"] = types.ObjectNull(sessionPersistenceTypes) - return nil - } - - sessionPersistenceMap := map[string]attr.Value{ - "use_source_ip_address": types.BoolPointerValue(sessionPersistenceResp.UseSourceIpAddress), - } - - sessionPersistenceTF, diags := types.ObjectValue(sessionPersistenceTypes, sessionPersistenceMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - - tp["session_persistence"] = sessionPersistenceTF - return nil -} diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go deleted file mode 100644 index 831ae1f4..00000000 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ /dev/null @@ -1,953 +0,0 @@ -package loadbalancer - -import ( - "context" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" -) - -const ( - testExternalAddress = "95.46.74.109" -) - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *loadbalancer.CreateLoadBalancerPayload - isValid bool - }{ - { - "default_values_ok", - &Model{}, - &loadbalancer.CreateLoadBalancerPayload{ - ExternalAddress: nil, - Listeners: nil, - Name: nil, - Networks: nil, - Options: &loadbalancer.LoadBalancerOptions{ - AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: nil, - }, - PrivateNetworkOnly: nil, - Observability: &loadbalancer.LoadbalancerOptionObservability{}, - }, - TargetPools: nil, - }, - true, - }, - { - "simple_values_ok", - &Model{ - ExternalAddress: types.StringValue("external_address"), - Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{ - types.ObjectValueMust(listenerTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "port": types.Int64Value(80), - "protocol": types.StringValue(string(loadbalancer.LISTENERPROTOCOL_TCP)), - "server_name_indicators": types.ListValueMust(types.ObjectType{AttrTypes: serverNameIndicatorTypes}, []attr.Value{ - types.ObjectValueMust( - serverNameIndicatorTypes, - map[string]attr.Value{ - "name": types.StringValue("domain.com"), - }, - ), - }, - ), - "target_pool": types.StringValue("target_pool"), - "tcp": types.ObjectValueMust(tcpTypes, map[string]attr.Value{ - "idle_timeout": types.StringValue("50s"), - }), - "udp": types.ObjectValueMust(udpTypes, map[string]attr.Value{ - "idle_timeout": types.StringValue("50s"), - }), - }), - }), - Name: types.StringValue("name"), - Networks: types.ListValueMust(types.ObjectType{AttrTypes: networkTypes}, []attr.Value{ - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id_2"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - }), - Options: types.ObjectValueMust( - optionsTypes, - map[string]attr.Value{ - "acl": types.SetValueMust( - types.StringType, - []attr.Value{types.StringValue("cidr")}), - "private_network_only": types.BoolValue(true), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("logs-credentials_ref"), - "push_url": types.StringValue("logs-push_url"), - }), - "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("metrics-credentials_ref"), - "push_url": types.StringValue("metrics-push_url"), - }), - }), - }, - ), - TargetPools: types.ListValueMust(types.ObjectType{AttrTypes: targetPoolTypes}, []attr.Value{ - types.ObjectValueMust(targetPoolTypes, map[string]attr.Value{ - "active_health_check": types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{ - "healthy_threshold": types.Int64Value(1), - "interval": types.StringValue("2s"), - "interval_jitter": types.StringValue("3s"), - "timeout": types.StringValue("4s"), - "unhealthy_threshold": types.Int64Value(5), - }), - "name": types.StringValue("name"), - "target_port": types.Int64Value(80), - "targets": types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{ - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "ip": types.StringValue("ip"), - }), - }), - "session_persistence": types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{ - "use_source_ip_address": types.BoolValue(true), - }), - }), - }), - }, - &loadbalancer.CreateLoadBalancerPayload{ - ExternalAddress: utils.Ptr("external_address"), - Listeners: &[]loadbalancer.Listener{ - { - DisplayName: utils.Ptr("display_name"), - Port: utils.Ptr(int64(80)), - Protocol: loadbalancer.LISTENERPROTOCOL_TCP.Ptr(), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ - { - Name: utils.Ptr("domain.com"), - }, - }, - TargetPool: utils.Ptr("target_pool"), - Tcp: &loadbalancer.OptionsTCP{ - IdleTimeout: utils.Ptr("50s"), - }, - Udp: &loadbalancer.OptionsUDP{ - IdleTimeout: utils.Ptr("50s"), - }, - }, - }, - Name: utils.Ptr("name"), - Networks: &[]loadbalancer.Network{ - { - NetworkId: utils.Ptr("network_id"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - { - NetworkId: utils.Ptr("network_id_2"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - }, - Options: &loadbalancer.LoadBalancerOptions{ - AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: &[]string{"cidr"}, - }, - PrivateNetworkOnly: utils.Ptr(true), - Observability: &loadbalancer.LoadbalancerOptionObservability{ - Logs: &loadbalancer.LoadbalancerOptionLogs{ - CredentialsRef: utils.Ptr("logs-credentials_ref"), - PushUrl: utils.Ptr("logs-push_url"), - }, - Metrics: &loadbalancer.LoadbalancerOptionMetrics{ - CredentialsRef: utils.Ptr("metrics-credentials_ref"), - PushUrl: utils.Ptr("metrics-push_url"), - }, - }, - }, - TargetPools: &[]loadbalancer.TargetPool{ - { - ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(1)), - Interval: utils.Ptr("2s"), - IntervalJitter: utils.Ptr("3s"), - Timeout: utils.Ptr("4s"), - UnhealthyThreshold: utils.Ptr(int64(5)), - }, - Name: utils.Ptr("name"), - TargetPort: utils.Ptr(int64(80)), - Targets: &[]loadbalancer.Target{ - { - DisplayName: utils.Ptr("display_name"), - Ip: utils.Ptr("ip"), - }, - }, - SessionPersistence: &loadbalancer.SessionPersistence{ - UseSourceIpAddress: utils.Ptr(true), - }, - }, - }, - }, - true, - }, - { - "service_plan_ok", - &Model{ - PlanId: types.StringValue("p10"), - ExternalAddress: types.StringValue("external_address"), - Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{ - types.ObjectValueMust(listenerTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "port": types.Int64Value(80), - "protocol": types.StringValue(string(loadbalancer.LISTENERPROTOCOL_TCP)), - "server_name_indicators": types.ListValueMust(types.ObjectType{AttrTypes: serverNameIndicatorTypes}, []attr.Value{ - types.ObjectValueMust( - serverNameIndicatorTypes, - map[string]attr.Value{ - "name": types.StringValue("domain.com"), - }, - ), - }, - ), - "target_pool": types.StringValue("target_pool"), - "tcp": types.ObjectNull(tcpTypes), - "udp": types.ObjectNull(udpTypes), - }), - }), - Name: types.StringValue("name"), - Networks: types.ListValueMust(types.ObjectType{AttrTypes: networkTypes}, []attr.Value{ - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id_2"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - }), - Options: types.ObjectValueMust( - optionsTypes, - map[string]attr.Value{ - "acl": types.SetValueMust( - types.StringType, - []attr.Value{types.StringValue("cidr")}), - "private_network_only": types.BoolValue(true), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("logs-credentials_ref"), - "push_url": types.StringValue("logs-push_url"), - }), - "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("metrics-credentials_ref"), - "push_url": types.StringValue("metrics-push_url"), - }), - }), - }, - ), - TargetPools: types.ListValueMust(types.ObjectType{AttrTypes: targetPoolTypes}, []attr.Value{ - types.ObjectValueMust(targetPoolTypes, map[string]attr.Value{ - "active_health_check": types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{ - "healthy_threshold": types.Int64Value(1), - "interval": types.StringValue("2s"), - "interval_jitter": types.StringValue("3s"), - "timeout": types.StringValue("4s"), - "unhealthy_threshold": types.Int64Value(5), - }), - "name": types.StringValue("name"), - "target_port": types.Int64Value(80), - "targets": types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{ - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "ip": types.StringValue("ip"), - }), - }), - "session_persistence": types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{ - "use_source_ip_address": types.BoolValue(true), - }), - }), - }), - }, - &loadbalancer.CreateLoadBalancerPayload{ - PlanId: utils.Ptr("p10"), - ExternalAddress: utils.Ptr("external_address"), - Listeners: &[]loadbalancer.Listener{ - { - DisplayName: utils.Ptr("display_name"), - Port: utils.Ptr(int64(80)), - Protocol: loadbalancer.LISTENERPROTOCOL_TCP.Ptr(), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ - { - Name: utils.Ptr("domain.com"), - }, - }, - TargetPool: utils.Ptr("target_pool"), - }, - }, - Name: utils.Ptr("name"), - Networks: &[]loadbalancer.Network{ - { - NetworkId: utils.Ptr("network_id"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - { - NetworkId: utils.Ptr("network_id_2"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - }, - Options: &loadbalancer.LoadBalancerOptions{ - AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: &[]string{"cidr"}, - }, - PrivateNetworkOnly: utils.Ptr(true), - Observability: &loadbalancer.LoadbalancerOptionObservability{ - Logs: &loadbalancer.LoadbalancerOptionLogs{ - CredentialsRef: utils.Ptr("logs-credentials_ref"), - PushUrl: utils.Ptr("logs-push_url"), - }, - Metrics: &loadbalancer.LoadbalancerOptionMetrics{ - CredentialsRef: utils.Ptr("metrics-credentials_ref"), - PushUrl: utils.Ptr("metrics-push_url"), - }, - }, - }, - TargetPools: &[]loadbalancer.TargetPool{ - { - ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(1)), - Interval: utils.Ptr("2s"), - IntervalJitter: utils.Ptr("3s"), - Timeout: utils.Ptr("4s"), - UnhealthyThreshold: utils.Ptr(int64(5)), - }, - Name: utils.Ptr("name"), - TargetPort: utils.Ptr(int64(80)), - Targets: &[]loadbalancer.Target{ - { - DisplayName: utils.Ptr("display_name"), - Ip: utils.Ptr("ip"), - }, - }, - SessionPersistence: &loadbalancer.SessionPersistence{ - UseSourceIpAddress: utils.Ptr(true), - }, - }, - }, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToTargetPoolUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *targetPool - expected *loadbalancer.UpdateTargetPoolPayload - isValid bool - }{ - { - "default_values_ok", - &targetPool{}, - &loadbalancer.UpdateTargetPoolPayload{}, - true, - }, - { - "simple_values_ok", - &targetPool{ - ActiveHealthCheck: types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{ - "healthy_threshold": types.Int64Value(1), - "interval": types.StringValue("2s"), - "interval_jitter": types.StringValue("3s"), - "timeout": types.StringValue("4s"), - "unhealthy_threshold": types.Int64Value(5), - }), - Name: types.StringValue("name"), - TargetPort: types.Int64Value(80), - Targets: types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{ - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "ip": types.StringValue("ip"), - }), - }), - SessionPersistence: types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{ - "use_source_ip_address": types.BoolValue(false), - }), - }, - &loadbalancer.UpdateTargetPoolPayload{ - ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(1)), - Interval: utils.Ptr("2s"), - IntervalJitter: utils.Ptr("3s"), - Timeout: utils.Ptr("4s"), - UnhealthyThreshold: utils.Ptr(int64(5)), - }, - Name: utils.Ptr("name"), - TargetPort: utils.Ptr(int64(80)), - Targets: &[]loadbalancer.Target{ - { - DisplayName: utils.Ptr("display_name"), - Ip: utils.Ptr("ip"), - }, - }, - SessionPersistence: &loadbalancer.SessionPersistence{ - UseSourceIpAddress: utils.Ptr(false), - }, - }, - true, - }, - { - "nil_target_pool", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toTargetPoolUpdatePayload(context.Background(), tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapFields(t *testing.T) { - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "name") - tests := []struct { - description string - input *loadbalancer.LoadBalancer - modelPrivateNetworkOnly *bool - region string - expected *Model - isValid bool - }{ - { - "default_values_ok", - &loadbalancer.LoadBalancer{ - ExternalAddress: nil, - Listeners: nil, - Name: utils.Ptr("name"), - Networks: nil, - Options: &loadbalancer.LoadBalancerOptions{ - AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: nil, - }, - PrivateNetworkOnly: nil, - Observability: &loadbalancer.LoadbalancerOptionObservability{ - Logs: &loadbalancer.LoadbalancerOptionLogs{}, - Metrics: &loadbalancer.LoadbalancerOptionMetrics{}, - }, - }, - TargetPools: nil, - }, - nil, - testRegion, - &Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - ExternalAddress: types.StringNull(), - Listeners: types.ListNull(types.ObjectType{AttrTypes: listenerTypes}), - Name: types.StringValue("name"), - Networks: types.ListNull(types.ObjectType{AttrTypes: networkTypes}), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "acl": types.SetNull(types.StringType), - "private_network_only": types.BoolNull(), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringNull(), - "push_url": types.StringNull(), - }), - "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringNull(), - "push_url": types.StringNull(), - }), - }), - }), - PrivateAddress: types.StringNull(), - SecurityGroupId: types.StringNull(), - TargetPools: types.ListNull(types.ObjectType{AttrTypes: targetPoolTypes}), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values_ok", - &loadbalancer.LoadBalancer{ - ExternalAddress: utils.Ptr("external_address"), - Listeners: utils.Ptr([]loadbalancer.Listener{ - { - DisplayName: utils.Ptr("display_name"), - Port: utils.Ptr(int64(80)), - Protocol: loadbalancer.LISTENERPROTOCOL_TCP.Ptr(), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ - { - Name: utils.Ptr("domain.com"), - }, - }, - TargetPool: utils.Ptr("target_pool"), - Tcp: &loadbalancer.OptionsTCP{ - IdleTimeout: utils.Ptr("50s"), - }, - Udp: &loadbalancer.OptionsUDP{ - IdleTimeout: utils.Ptr("50s"), - }, - }, - }), - Name: utils.Ptr("name"), - Networks: utils.Ptr([]loadbalancer.Network{ - { - NetworkId: utils.Ptr("network_id"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - { - NetworkId: utils.Ptr("network_id_2"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - }), - Options: utils.Ptr(loadbalancer.LoadBalancerOptions{ - PrivateNetworkOnly: utils.Ptr(true), - Observability: &loadbalancer.LoadbalancerOptionObservability{ - Logs: &loadbalancer.LoadbalancerOptionLogs{ - CredentialsRef: utils.Ptr("logs_credentials_ref"), - PushUrl: utils.Ptr("logs_push_url"), - }, - Metrics: &loadbalancer.LoadbalancerOptionMetrics{ - CredentialsRef: utils.Ptr("metrics_credentials_ref"), - PushUrl: utils.Ptr("metrics_push_url"), - }, - }, - }), - TargetSecurityGroup: loadbalancer.LoadBalancerGetTargetSecurityGroupAttributeType(&loadbalancer.SecurityGroup{ - Id: utils.Ptr("sg-id-12345"), - Name: utils.Ptr("sg-name-abcde"), - }), - TargetPools: utils.Ptr([]loadbalancer.TargetPool{ - { - ActiveHealthCheck: utils.Ptr(loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(1)), - Interval: utils.Ptr("2s"), - IntervalJitter: utils.Ptr("3s"), - Timeout: utils.Ptr("4s"), - UnhealthyThreshold: utils.Ptr(int64(5)), - }), - Name: utils.Ptr("name"), - TargetPort: utils.Ptr(int64(80)), - Targets: utils.Ptr([]loadbalancer.Target{ - { - DisplayName: utils.Ptr("display_name"), - Ip: utils.Ptr("ip"), - }, - }), - SessionPersistence: utils.Ptr(loadbalancer.SessionPersistence{ - UseSourceIpAddress: utils.Ptr(true), - }), - }, - }), - }, - nil, - testRegion, - &Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - ExternalAddress: types.StringValue("external_address"), - SecurityGroupId: types.StringValue("sg-id-12345"), - Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{ - types.ObjectValueMust(listenerTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "port": types.Int64Value(80), - "protocol": types.StringValue(string(loadbalancer.LISTENERPROTOCOL_TCP)), - "server_name_indicators": types.ListValueMust(types.ObjectType{AttrTypes: serverNameIndicatorTypes}, []attr.Value{ - types.ObjectValueMust( - serverNameIndicatorTypes, - map[string]attr.Value{ - "name": types.StringValue("domain.com"), - }, - ), - }, - ), - "target_pool": types.StringValue("target_pool"), - "tcp": types.ObjectValueMust(tcpTypes, map[string]attr.Value{ - "idle_timeout": types.StringValue("50s"), - }), - "udp": types.ObjectValueMust(udpTypes, map[string]attr.Value{ - "idle_timeout": types.StringValue("50s"), - }), - }), - }), - Name: types.StringValue("name"), - Networks: types.ListValueMust(types.ObjectType{AttrTypes: networkTypes}, []attr.Value{ - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id_2"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - }), - Options: types.ObjectValueMust( - optionsTypes, - map[string]attr.Value{ - "private_network_only": types.BoolValue(true), - "acl": types.SetNull(types.StringType), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("logs_credentials_ref"), - "push_url": types.StringValue("logs_push_url"), - }), - "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("metrics_credentials_ref"), - "push_url": types.StringValue("metrics_push_url"), - }), - }), - }, - ), - TargetPools: types.ListValueMust(types.ObjectType{AttrTypes: targetPoolTypes}, []attr.Value{ - types.ObjectValueMust(targetPoolTypes, map[string]attr.Value{ - "active_health_check": types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{ - "healthy_threshold": types.Int64Value(1), - "interval": types.StringValue("2s"), - "interval_jitter": types.StringValue("3s"), - "timeout": types.StringValue("4s"), - "unhealthy_threshold": types.Int64Value(5), - }), - "name": types.StringValue("name"), - "target_port": types.Int64Value(80), - "targets": types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{ - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "ip": types.StringValue("ip"), - }), - }), - "session_persistence": types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{ - "use_source_ip_address": types.BoolValue(true), - }), - }), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values_ok_with_null_private_network_only_response", - &loadbalancer.LoadBalancer{ - ExternalAddress: utils.Ptr("external_address"), - Listeners: utils.Ptr([]loadbalancer.Listener{ - { - DisplayName: utils.Ptr("display_name"), - Port: utils.Ptr(int64(80)), - Protocol: loadbalancer.LISTENERPROTOCOL_TCP.Ptr(), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ - { - Name: utils.Ptr("domain.com"), - }, - }, - TargetPool: utils.Ptr("target_pool"), - }, - }), - Name: utils.Ptr("name"), - Networks: utils.Ptr([]loadbalancer.Network{ - { - NetworkId: utils.Ptr("network_id"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - { - NetworkId: utils.Ptr("network_id_2"), - Role: loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS.Ptr(), - }, - }), - Options: utils.Ptr(loadbalancer.LoadBalancerOptions{ - AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: utils.Ptr([]string{"cidr"}), - }, - PrivateNetworkOnly: nil, // API sets this to nil if it's false in the request - Observability: &loadbalancer.LoadbalancerOptionObservability{ - Logs: &loadbalancer.LoadbalancerOptionLogs{ - CredentialsRef: utils.Ptr("logs_credentials_ref"), - PushUrl: utils.Ptr("logs_push_url"), - }, - Metrics: &loadbalancer.LoadbalancerOptionMetrics{ - CredentialsRef: utils.Ptr("metrics_credentials_ref"), - PushUrl: utils.Ptr("metrics_push_url"), - }, - }, - }), - TargetPools: utils.Ptr([]loadbalancer.TargetPool{ - { - ActiveHealthCheck: utils.Ptr(loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(1)), - Interval: utils.Ptr("2s"), - IntervalJitter: utils.Ptr("3s"), - Timeout: utils.Ptr("4s"), - UnhealthyThreshold: utils.Ptr(int64(5)), - }), - Name: utils.Ptr("name"), - TargetPort: utils.Ptr(int64(80)), - Targets: utils.Ptr([]loadbalancer.Target{ - { - DisplayName: utils.Ptr("display_name"), - Ip: utils.Ptr("ip"), - }, - }), - SessionPersistence: utils.Ptr(loadbalancer.SessionPersistence{ - UseSourceIpAddress: utils.Ptr(true), - }), - }, - }), - }, - utils.Ptr(false), - testRegion, - &Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - ExternalAddress: types.StringValue("external_address"), - Listeners: types.ListValueMust(types.ObjectType{AttrTypes: listenerTypes}, []attr.Value{ - types.ObjectValueMust(listenerTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "port": types.Int64Value(80), - "protocol": types.StringValue(string(loadbalancer.LISTENERPROTOCOL_TCP)), - "server_name_indicators": types.ListValueMust(types.ObjectType{AttrTypes: serverNameIndicatorTypes}, []attr.Value{ - types.ObjectValueMust( - serverNameIndicatorTypes, - map[string]attr.Value{ - "name": types.StringValue("domain.com"), - }, - ), - }, - ), - "target_pool": types.StringValue("target_pool"), - "tcp": types.ObjectNull(tcpTypes), - "udp": types.ObjectNull(udpTypes), - }), - }), - Name: types.StringValue("name"), - Networks: types.ListValueMust(types.ObjectType{AttrTypes: networkTypes}, []attr.Value{ - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "network_id": types.StringValue("network_id_2"), - "role": types.StringValue(string(loadbalancer.NETWORKROLE_LISTENERS_AND_TARGETS)), - }), - }), - Options: types.ObjectValueMust( - optionsTypes, - map[string]attr.Value{ - "acl": types.SetValueMust( - types.StringType, - []attr.Value{types.StringValue("cidr")}), - "private_network_only": types.BoolValue(false), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("logs_credentials_ref"), - "push_url": types.StringValue("logs_push_url"), - }), - "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringValue("metrics_credentials_ref"), - "push_url": types.StringValue("metrics_push_url"), - }), - }), - }, - ), - TargetPools: types.ListValueMust(types.ObjectType{AttrTypes: targetPoolTypes}, []attr.Value{ - types.ObjectValueMust(targetPoolTypes, map[string]attr.Value{ - "active_health_check": types.ObjectValueMust(activeHealthCheckTypes, map[string]attr.Value{ - "healthy_threshold": types.Int64Value(1), - "interval": types.StringValue("2s"), - "interval_jitter": types.StringValue("3s"), - "timeout": types.StringValue("4s"), - "unhealthy_threshold": types.Int64Value(5), - }), - "name": types.StringValue("name"), - "target_port": types.Int64Value(80), - "targets": types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{ - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "display_name": types.StringValue("display_name"), - "ip": types.StringValue("ip"), - }), - }), - "session_persistence": types.ObjectValueMust(sessionPersistenceTypes, map[string]attr.Value{ - "use_source_ip_address": types.BoolValue(true), - }), - }), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - nil, - testRegion, - &Model{}, - false, - }, - { - "no_name", - &loadbalancer.LoadBalancer{}, - nil, - testRegion, - &Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - } - if tt.modelPrivateNetworkOnly != nil { - model.Options = types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "private_network_only": types.BoolValue(*tt.modelPrivateNetworkOnly), - "acl": types.SetNull(types.StringType), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "logs": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringNull(), - "push_url": types.StringNull(), - }), - "metrics": types.ObjectValueMust(observabilityOptionTypes, map[string]attr.Value{ - "credentials_ref": types.StringNull(), - "push_url": types.StringNull(), - }), - }), - }) - } - err := mapFields(context.Background(), tt.input, model, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func Test_validateConfig(t *testing.T) { - type args struct { - ExternalAddress *string - PrivateNetworkOnly *bool - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - name: "happy case 1: private_network_only is not set and external_address is set", - args: args{ - ExternalAddress: utils.Ptr(testExternalAddress), - PrivateNetworkOnly: nil, - }, - wantErr: false, - }, - { - name: "happy case 2: private_network_only is set to false and external_address is set", - args: args{ - ExternalAddress: utils.Ptr(testExternalAddress), - PrivateNetworkOnly: utils.Ptr(false), - }, - wantErr: false, - }, - { - name: "happy case 3: private_network_only is set to true and external_address is not set", - args: args{ - ExternalAddress: nil, - PrivateNetworkOnly: utils.Ptr(true), - }, - wantErr: false, - }, - { - name: "error case 1: private_network_only and external_address are set", - args: args{ - ExternalAddress: utils.Ptr(testExternalAddress), - PrivateNetworkOnly: utils.Ptr(true), - }, - wantErr: true, - }, - { - name: "error case 2: private_network_only is not set and external_address is not set", - args: args{ - ExternalAddress: nil, - PrivateNetworkOnly: nil, - }, - wantErr: true, - }, - { - name: "error case 3: private_network_only is set to false and external_address is not set", - args: args{ - ExternalAddress: nil, - PrivateNetworkOnly: utils.Ptr(false), - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - model := &Model{ - ExternalAddress: types.StringPointerValue(tt.args.ExternalAddress), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "acl": types.SetNull(types.StringType), - "observability": types.ObjectNull(observabilityTypes), - "private_network_only": types.BoolPointerValue(tt.args.PrivateNetworkOnly), - }), - } - - validateConfig(ctx, &diags, model) - - if diags.HasError() != tt.wantErr { - t.Errorf("validateConfig() = %v, want %v", diags.HasError(), tt.wantErr) - } - }) - } -} diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go deleted file mode 100644 index 90905338..00000000 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ /dev/null @@ -1,466 +0,0 @@ -package loadbalancer_test - -import ( - "context" - _ "embed" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - - "maps" - - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -//go:embed testfiles/resource-min.tf -var resourceMinConfig string - -//go:embed testfiles/resource-max.tf -var resourceMaxConfig string - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "plan_id": config.StringVariable("p10"), - "disable_security_group_assignment": config.BoolVariable(false), - "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "target_pool_name": config.StringVariable("example-target-pool"), - "target_port": config.StringVariable("5432"), - "target_display_name": config.StringVariable("example-target"), - "listener_port": config.StringVariable("5432"), - "listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), - "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), - - "obs_display_name": config.StringVariable("obs-user"), - "obs_username": config.StringVariable("obs-username"), - "obs_password": config.StringVariable("obs-password1"), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "plan_id": config.StringVariable("p10"), - "disable_security_group_assignment": config.BoolVariable(true), - "network_name": config.StringVariable(fmt.Sprintf("tf-acc-n%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "network_role": config.StringVariable("ROLE_LISTENERS_AND_TARGETS"), - "server_name": config.StringVariable(fmt.Sprintf("tf-acc-s%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "loadbalancer_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - - "target_display_name": config.StringVariable("example-target"), - - "sni_target_pool_name": config.StringVariable("example-target-pool"), - "sni_target_port": config.StringVariable("5432"), - "sni_listener_port": config.StringVariable("5432"), - "sni_listener_protocol": config.StringVariable("PROTOCOL_TLS_PASSTHROUGH"), - "sni_idle_timeout": config.StringVariable("42s"), - "sni_listener_display_name": config.StringVariable("example-listener"), - "sni_listener_server_name_indicators": config.StringVariable("acc-test.runs.onstackit.cloud"), - "sni_healthy_threshold": config.StringVariable("3"), - "sni_health_interval": config.StringVariable("10s"), - "sni_health_interval_jitter": config.StringVariable("5s"), - "sni_health_timeout": config.StringVariable("10s"), - "sni_unhealthy_threshold": config.StringVariable("3"), - "sni_use_source_ip_address": config.StringVariable("true"), - - "udp_target_pool_name": config.StringVariable("udp-target-pool"), - "udp_target_port": config.StringVariable("53"), - "udp_listener_port": config.StringVariable("53"), - "udp_listener_protocol": config.StringVariable("PROTOCOL_UDP"), - "udp_idle_timeout": config.StringVariable("43s"), - "udp_listener_display_name": config.StringVariable("udp-listener"), - - "private_network_only": config.StringVariable("false"), - "acl": config.StringVariable("192.168.0.0/24"), - - "observability_logs_push_url": config.StringVariable("https://logs.observability.dummy.stackit.cloud"), - "observability_metrics_push_url": config.StringVariable("https://metrics.observability.dummy.stackit.cloud"), - "observability_credential_logs_name": config.StringVariable(fmt.Sprintf("tf-acc-l%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "observability_credential_logs_username": config.StringVariable("obs-cred-logs-username"), - "observability_credential_logs_password": config.StringVariable("obs-cred-logs-password"), - "observability_credential_metrics_name": config.StringVariable(fmt.Sprintf("tf-acc-m%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "observability_credential_metrics_username": config.StringVariable("obs-cred-metrics-username"), - "observability_credential_metrics_password": config.StringVariable("obs-cred-metrics-password"), -} - -func configVarsMinUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMin)) - maps.Copy(tempConfig, testConfigVarsMin) - tempConfig["target_port"] = config.StringVariable("5431") - return tempConfig -} - -func configVarsMaxUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMax)) - maps.Copy(tempConfig, testConfigVarsMax) - tempConfig["sni_target_port"] = config.StringVariable("5431") - return tempConfig -} - -func TestAccLoadBalancerResourceMin(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckLoadBalancerDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMin, - Config: testutil.LoadBalancerProviderConfig() + resourceMinConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Load balancer instance resource - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMin["loadbalancer_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMin["target_pool_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(testConfigVarsMin["target_port"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMin["target_display_name"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.ip"), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "listeners.0.display_name"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.port", testutil.ConvertConfigVariable(testConfigVarsMin["listener_port"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.protocol", testutil.ConvertConfigVariable(testConfigVarsMin["listener_protocol"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.target_pool", testutil.ConvertConfigVariable(testConfigVarsMin["target_pool_name"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), - resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), - resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), - resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), - resource.TestCheckNoResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), - - // Loadbalancer observability credentials resource - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "display_name", testutil.ConvertConfigVariable(testConfigVarsMin["obs_display_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "username", testutil.ConvertConfigVariable(testConfigVarsMin["obs_username"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.obs_credential", "password", testutil.ConvertConfigVariable(testConfigVarsMin["obs_password"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer_observability_credential.obs_credential", "credentials_ref"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMin, - Config: fmt.Sprintf(` - %s - - data "stackit_loadbalancer" "loadbalancer" { - project_id = stackit_loadbalancer.loadbalancer.project_id - name = stackit_loadbalancer.loadbalancer.name - } - `, - testutil.LoadBalancerProviderConfig()+resourceMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Load balancer instance - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMin["loadbalancer_name"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMin["plan_id"])), - resource.TestCheckResourceAttrPair( - "data.stackit_loadbalancer.loadbalancer", "project_id", - "stackit_loadbalancer.loadbalancer", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_loadbalancer.loadbalancer", "name", - "stackit_loadbalancer.loadbalancer", "name", - ), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMin["target_pool_name"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(testConfigVarsMin["target_port"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMin["target_display_name"])), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.ip"), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "listeners.0.display_name"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.port", testutil.ConvertConfigVariable(testConfigVarsMin["listener_port"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.protocol", testutil.ConvertConfigVariable(testConfigVarsMin["listener_protocol"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.target_pool", testutil.ConvertConfigVariable(testConfigVarsMin["target_pool_name"])), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMin["network_role"])), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", "false"), - resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), - resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url"), - resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), - resource.TestCheckNoResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url"), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "security_group_id"), - resource.TestCheckResourceAttrPair( - "stackit_loadbalancer.loadbalancer", "security_group_id", - "data.stackit_loadbalancer.loadbalancer", "security_group_id", - ), - )}, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_loadbalancer.loadbalancer", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_loadbalancer.loadbalancer"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_loadbalancer.loadbalancer") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, name), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"options.private_network_only"}, - }, - // Update - { - ConfigVariables: configVarsMinUpdated(), - Config: testutil.LoadBalancerProviderConfig() + resourceMinConfig, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMin["loadbalancer_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(configVarsMinUpdated()["target_port"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccLoadBalancerResourceMax(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckLoadBalancerDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMax, - Config: testutil.LoadBalancerProviderConfig() + resourceMaxConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Load balancer instance resource - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMax["loadbalancer_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMax["plan_id"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "networks.0.network_id"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), - - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_display_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.port", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_port"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.protocol", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_protocol"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.target_pool", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_pool_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.server_name_indicators.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_server_name_indicators"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.0.tcp.idle_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["sni_idle_timeout"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_pool_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_port"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["target_display_name"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.ip"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["sni_healthy_threshold"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["sni_health_interval"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval_jitter", testutil.ConvertConfigVariable(testConfigVarsMax["sni_health_interval_jitter"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.timeout", testutil.ConvertConfigVariable(testConfigVarsMax["sni_health_timeout"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.unhealthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["sni_unhealthy_threshold"])), - - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["udp_listener_display_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.port", testutil.ConvertConfigVariable(testConfigVarsMax["udp_listener_port"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.protocol", testutil.ConvertConfigVariable(testConfigVarsMax["udp_listener_protocol"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.target_pool", testutil.ConvertConfigVariable(testConfigVarsMax["udp_target_pool_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.udp.idle_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["udp_idle_timeout"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.1.name", testutil.ConvertConfigVariable(testConfigVarsMax["udp_target_pool_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.1.target_port", testutil.ConvertConfigVariable(testConfigVarsMax["udp_target_port"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.1.targets.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["target_display_name"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "target_pools.1.targets.0.ip"), - - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.session_persistence.use_source_ip_address", testutil.ConvertConfigVariable(testConfigVarsMax["sni_use_source_ip_address"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.private_network_only", testutil.ConvertConfigVariable(testConfigVarsMax["private_network_only"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), - - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), - resource.TestCheckResourceAttrPair("stackit_loadbalancer_observability_credential.logs", "credentials_ref", "stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url", testutil.ConvertConfigVariable(testConfigVarsMax["observability_logs_push_url"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), - resource.TestCheckResourceAttrPair("stackit_loadbalancer_observability_credential.metrics", "credentials_ref", "stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url", testutil.ConvertConfigVariable(testConfigVarsMax["observability_metrics_push_url"])), - - // Loadbalancer observability credential resource - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "display_name", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_logs_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "username", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_logs_username"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.logs", "password", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_logs_password"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer_observability_credential.logs", "credentials_ref"), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "display_name", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_metrics_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "username", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_metrics_username"])), - resource.TestCheckResourceAttr("stackit_loadbalancer_observability_credential.metrics", "password", testutil.ConvertConfigVariable(testConfigVarsMax["observability_credential_metrics_password"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer_observability_credential.metrics", "credentials_ref"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMax, - Config: fmt.Sprintf(` - %s - - data "stackit_loadbalancer" "loadbalancer" { - project_id = stackit_loadbalancer.loadbalancer.project_id - name = stackit_loadbalancer.loadbalancer.name - } - `, - testutil.LoadBalancerProviderConfig()+resourceMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Load balancer instance - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMax["loadbalancer_name"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMax["plan_id"])), - resource.TestCheckResourceAttrPair( - "data.stackit_loadbalancer.loadbalancer", "project_id", - "stackit_loadbalancer.loadbalancer", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_loadbalancer.loadbalancer", "name", - "stackit_loadbalancer.loadbalancer", "name", - ), - // Load balancer instance - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "networks.0.network_id"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "networks.0.role", testutil.ConvertConfigVariable(testConfigVarsMax["network_role"])), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "external_address"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "disable_security_group_assignment", testutil.ConvertConfigVariable(testConfigVarsMax["disable_security_group_assignment"])), - resource.TestCheckResourceAttrSet("stackit_loadbalancer.loadbalancer", "security_group_id"), - - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_pool_name"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_port"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["target_display_name"])), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "target_pools.0.targets.0.ip"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_display_name"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.port", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_port"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.protocol", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_protocol"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.target_pool", testutil.ConvertConfigVariable(testConfigVarsMax["sni_target_pool_name"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.server_name_indicators.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["sni_listener_server_name_indicators"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "listeners.0.tcp.idle_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["sni_idle_timeout"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.healthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["sni_healthy_threshold"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval", testutil.ConvertConfigVariable(testConfigVarsMax["sni_health_interval"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.interval_jitter", testutil.ConvertConfigVariable(testConfigVarsMax["sni_health_interval_jitter"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.timeout", testutil.ConvertConfigVariable(testConfigVarsMax["sni_health_timeout"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.active_health_check.unhealthy_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["sni_unhealthy_threshold"])), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "target_pools.0.session_persistence.use_source_ip_address", testutil.ConvertConfigVariable(testConfigVarsMax["sni_use_source_ip_address"])), - - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.display_name", testutil.ConvertConfigVariable(testConfigVarsMax["udp_listener_display_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.port", testutil.ConvertConfigVariable(testConfigVarsMax["udp_listener_port"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.protocol", testutil.ConvertConfigVariable(testConfigVarsMax["udp_listener_protocol"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.target_pool", testutil.ConvertConfigVariable(testConfigVarsMax["udp_target_pool_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "listeners.1.udp.idle_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["udp_idle_timeout"])), - - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), - resource.TestCheckResourceAttrPair("stackit_loadbalancer_observability_credential.logs", "credentials_ref", "data.stackit_loadbalancer.loadbalancer", "options.observability.logs.credentials_ref"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.logs.push_url", testutil.ConvertConfigVariable(testConfigVarsMax["observability_logs_push_url"])), - resource.TestCheckResourceAttrSet("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), - resource.TestCheckResourceAttrPair("stackit_loadbalancer_observability_credential.metrics", "credentials_ref", "data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.credentials_ref"), - resource.TestCheckResourceAttr("data.stackit_loadbalancer.loadbalancer", "options.observability.metrics.push_url", testutil.ConvertConfigVariable(testConfigVarsMax["observability_metrics_push_url"])), - resource.TestCheckResourceAttrPair( - "stackit_loadbalancer.loadbalancer", "security_group_id", - "data.stackit_loadbalancer.loadbalancer", "security_group_id", - ), - )}, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_loadbalancer.loadbalancer", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_loadbalancer.loadbalancer"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_loadbalancer.loadbalancer") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, name), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"options.private_network_only"}, - }, - // Update - { - ConfigVariables: configVarsMaxUpdated(), - Config: testutil.LoadBalancerProviderConfig() + resourceMaxConfig, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "name", testutil.ConvertConfigVariable(testConfigVarsMax["loadbalancer_name"])), - resource.TestCheckResourceAttr("stackit_loadbalancer.loadbalancer", "target_pools.0.target_port", testutil.ConvertConfigVariable(configVarsMaxUpdated()["sni_target_port"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckLoadBalancerDestroy(s *terraform.State) error { - ctx := context.Background() - var client *loadbalancer.APIClient - var err error - if testutil.LoadBalancerCustomEndpoint == "" { - client, err = loadbalancer.NewAPIClient() - } else { - client, err = loadbalancer.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.LoadBalancerCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - region := "eu01" - if testutil.Region != "" { - region = testutil.Region - } - loadbalancersToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_loadbalancer" { - continue - } - // loadbalancer terraform ID: = "[project_id],[name]" - loadbalancerName := strings.Split(rs.Primary.ID, core.Separator)[1] - loadbalancersToDestroy = append(loadbalancersToDestroy, loadbalancerName) - } - - loadbalancersResp, err := client.ListLoadBalancers(ctx, testutil.ProjectId, region).Execute() - if err != nil { - return fmt.Errorf("getting loadbalancersResp: %w", err) - } - - if loadbalancersResp.LoadBalancers == nil || (loadbalancersResp.LoadBalancers != nil && len(*loadbalancersResp.LoadBalancers) == 0) { - fmt.Print("No load balancers found for project \n") - return nil - } - - items := *loadbalancersResp.LoadBalancers - for i := range items { - if items[i].Name == nil { - continue - } - if utils.Contains(loadbalancersToDestroy, *items[i].Name) { - _, err := client.DeleteLoadBalancerExecute(ctx, testutil.ProjectId, region, *items[i].Name) - if err != nil { - return fmt.Errorf("destroying load balancer %s during CheckDestroy: %w", *items[i].Name, err) - } - _, err = wait.DeleteLoadBalancerWaitHandler(ctx, client, testutil.ProjectId, region, *items[i].Name).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying load balancer %s during CheckDestroy: waiting for deletion %w", *items[i].Name, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/loadbalancer/observability-credential/resource.go b/stackit/internal/services/loadbalancer/observability-credential/resource.go deleted file mode 100644 index e4613c06..00000000 --- a/stackit/internal/services/loadbalancer/observability-credential/resource.go +++ /dev/null @@ -1,383 +0,0 @@ -package loadbalancer - -import ( - "context" - "fmt" - "net/http" - "strings" - - loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &observabilityCredentialResource{} - _ resource.ResourceWithConfigure = &observabilityCredentialResource{} - _ resource.ResourceWithImportState = &observabilityCredentialResource{} - _ resource.ResourceWithModifyPlan = &observabilityCredentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - DisplayName types.String `tfsdk:"display_name"` - Username types.String `tfsdk:"username"` - Password types.String `tfsdk:"password"` - CredentialsRef types.String `tfsdk:"credentials_ref"` - Region types.String `tfsdk:"region"` -} - -// NewObservabilityCredentialResource is a helper function to simplify the provider implementation. -func NewObservabilityCredentialResource() resource.Resource { - return &observabilityCredentialResource{} -} - -// observabilityCredentialResource is the resource implementation. -type observabilityCredentialResource struct { - client *loadbalancer.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *observabilityCredentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_loadbalancer_observability_credential" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *observabilityCredentialResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Configure adds the provider configured client to the resource. -func (r *observabilityCredentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := loadbalancerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Load Balancer client configured") -} - -// Schema defines the schema for the resource. -func (r *observabilityCredentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Load balancer observability credential resource schema. Must have a `region` specified in the provider configuration. These contain the username and password for the observability service (e.g. Argus) where the load balancer logs/metrics will be pushed into", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`\",\"region\",\"`credentials_ref`\".", - "credentials_ref": "The credentials reference is used by the Load Balancer to define which credentials it will use.", - "project_id": "STACKIT project ID to which the load balancer observability credential is associated.", - "display_name": "Observability credential name.", - "username": "The password for the observability service (e.g. Argus) where the logs/metrics will be pushed into.", - "password": "The username for the observability service (e.g. Argus) where the logs/metrics will be pushed into.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credentials_ref": schema.StringAttribute{ - Description: descriptions["credentials_ref"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "display_name": schema.StringAttribute{ - Description: descriptions["display_name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "password": schema.StringAttribute{ - Description: descriptions["password"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *observabilityCredentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating observability credential", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new observability credentials - createResp, err := r.client.CreateCredentials(ctx, projectId, region).CreateCredentialsPayload(*payload).XRequestID(uuid.NewString()).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating observability credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - ctx = tflog.SetField(ctx, "credentials_ref", createResp.Credential.CredentialsRef) - - // Map response body to schema - err = mapFields(createResp.Credential, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating observability credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Load balancer observability credential created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *observabilityCredentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsRef := model.CredentialsRef.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_ref", credentialsRef) - ctx = tflog.SetField(ctx, "region", region) - - // Get credentials - credResp, err := r.client.GetCredentials(ctx, projectId, region, credentialsRef).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading observability credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(credResp.Credential, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading observability credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Load balancer observability credential read") -} - -func (r *observabilityCredentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating observability credential", "Observability credential can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *observabilityCredentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsRef := model.CredentialsRef.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_ref", credentialsRef) - ctx = tflog.SetField(ctx, "region", region) - - // Delete credentials - _, err := r.client.DeleteCredentials(ctx, projectId, region, credentialsRef).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting observability credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Load balancer observability credential deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name -func (r *observabilityCredentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing observability credential", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[credentials_ref] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credentials_ref"), idParts[2])...) - tflog.Info(ctx, "Load balancer observability credential state imported") -} - -func toCreatePayload(model *Model) (*loadbalancer.CreateCredentialsPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &loadbalancer.CreateCredentialsPayload{ - DisplayName: conversion.StringValueToPointer(model.DisplayName), - Username: conversion.StringValueToPointer(model.Username), - Password: conversion.StringValueToPointer(model.Password), - }, nil -} - -func mapFields(cred *loadbalancer.CredentialsResponse, m *Model, region string) error { - if cred == nil { - return fmt.Errorf("response input is nil") - } - if m == nil { - return fmt.Errorf("model input is nil") - } - - var credentialsRef string - if m.CredentialsRef.ValueString() != "" { - credentialsRef = m.CredentialsRef.ValueString() - } else if cred.CredentialsRef != nil { - credentialsRef = *cred.CredentialsRef - } else { - return fmt.Errorf("credentials ref not present") - } - m.CredentialsRef = types.StringValue(credentialsRef) - m.DisplayName = types.StringPointerValue(cred.DisplayName) - var username string - if m.Username.ValueString() != "" { - username = m.Username.ValueString() - } else if cred.Username != nil { - username = *cred.Username - } else { - return fmt.Errorf("username not present") - } - m.Username = types.StringValue(username) - m.Region = types.StringValue(region) - m.Id = utils.BuildInternalTerraformId( - m.ProjectId.ValueString(), - m.Region.ValueString(), - m.CredentialsRef.ValueString(), - ) - - return nil -} diff --git a/stackit/internal/services/loadbalancer/observability-credential/resource_test.go b/stackit/internal/services/loadbalancer/observability-credential/resource_test.go deleted file mode 100644 index a6aaffe7..00000000 --- a/stackit/internal/services/loadbalancer/observability-credential/resource_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package loadbalancer - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" -) - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *loadbalancer.CreateCredentialsPayload - isValid bool - }{ - { - "default_values_ok", - &Model{}, - &loadbalancer.CreateCredentialsPayload{ - DisplayName: nil, - Username: nil, - Password: nil, - }, - true, - }, - { - "simple_values_ok", - &Model{ - DisplayName: types.StringValue("display_name"), - Username: types.StringValue("username"), - Password: types.StringValue("password"), - }, - &loadbalancer.CreateCredentialsPayload{ - DisplayName: utils.Ptr("display_name"), - Username: utils.Ptr("username"), - Password: utils.Ptr("password"), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapFields(t *testing.T) { - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "credentials_ref") - tests := []struct { - description string - input *loadbalancer.CredentialsResponse - region string - expected *Model - isValid bool - }{ - { - "default_values_ok", - &loadbalancer.CredentialsResponse{ - CredentialsRef: utils.Ptr("credentials_ref"), - Username: utils.Ptr("username"), - }, - testRegion, - &Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsRef: types.StringValue("credentials_ref"), - Username: types.StringValue("username"), - Region: types.StringValue(testRegion), - }, - true, - }, - - { - "simple_values_ok", - &loadbalancer.CredentialsResponse{ - CredentialsRef: utils.Ptr("credentials_ref"), - DisplayName: utils.Ptr("display_name"), - Username: utils.Ptr("username"), - }, - testRegion, - &Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsRef: types.StringValue("credentials_ref"), - DisplayName: types.StringValue("display_name"), - Username: types.StringValue("username"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - &Model{}, - false, - }, - { - "no_username", - &loadbalancer.CredentialsResponse{ - CredentialsRef: utils.Ptr("credentials_ref"), - DisplayName: utils.Ptr("display_name"), - }, - testRegion, - &Model{}, - false, - }, - { - "no_credentials_ref", - &loadbalancer.CredentialsResponse{ - DisplayName: utils.Ptr("display_name"), - Username: utils.Ptr("username"), - }, - testRegion, - &Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - } - err := mapFields(tt.input, model, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf b/stackit/internal/services/loadbalancer/testfiles/resource-max.tf deleted file mode 100644 index d870cb62..00000000 --- a/stackit/internal/services/loadbalancer/testfiles/resource-max.tf +++ /dev/null @@ -1,188 +0,0 @@ - -variable "project_id" {} -variable "network_name" {} -variable "network_role" {} -variable "server_name" {} - -variable "loadbalancer_name" {} -variable "plan_id" {} -variable "disable_security_group_assignment" {} - -variable "target_display_name" {} - -variable "sni_target_pool_name" {} -variable "sni_target_port" {} -variable "sni_listener_port" {} -variable "sni_listener_protocol" {} -variable "sni_idle_timeout" {} -variable "sni_listener_display_name" {} -variable "sni_listener_server_name_indicators" {} -variable "sni_healthy_threshold" {} -variable "sni_health_interval" {} -variable "sni_health_interval_jitter" {} -variable "sni_health_timeout" {} -variable "sni_unhealthy_threshold" {} -variable "sni_use_source_ip_address" {} - -variable "udp_target_pool_name" {} -variable "udp_target_port" {} -variable "udp_listener_port" {} -variable "udp_listener_protocol" {} -variable "udp_idle_timeout" {} -variable "udp_listener_display_name" {} - -variable "private_network_only" {} -variable "acl" {} - -variable "observability_logs_push_url" {} -variable "observability_metrics_push_url" {} -variable "observability_credential_logs_name" {} -variable "observability_credential_logs_username" {} -variable "observability_credential_logs_password" {} -variable "observability_credential_metrics_name" {} -variable "observability_credential_metrics_username" {} -variable "observability_credential_metrics_password" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.network_name - ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.3.0/25" - routed = "true" -} - -resource "stackit_network_interface" "network_interface" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id - name = "name" - lifecycle { - ignore_changes = [ - security_group_ids, - ] - } -} - -resource "stackit_public_ip" "public_ip" { - project_id = var.project_id - network_interface_id = stackit_network_interface.network_interface.network_interface_id - lifecycle { - ignore_changes = [ - network_interface_id - ] - } -} - -resource "stackit_server" "server" { - project_id = var.project_id - availability_zone = "eu01-1" - name = var.server_name - machine_type = "t1.1" - boot_volume = { - size = 32 - source_type = "image" - source_id = "59838a89-51b1-4892-b57f-b3caf598ee2f" - delete_on_termination = "true" - } - network_interfaces = [stackit_network_interface.network_interface.network_interface_id] - user_data = "#!/bin/bash" -} - -resource "stackit_loadbalancer" "loadbalancer" { - project_id = var.project_id - name = var.loadbalancer_name - plan_id = var.plan_id - disable_security_group_assignment = var.disable_security_group_assignment - target_pools = [ - { - name = var.sni_target_pool_name - target_port = var.sni_target_port - targets = [ - { - display_name = var.target_display_name - ip = stackit_network_interface.network_interface.ipv4 - } - ] - active_health_check = { - healthy_threshold = var.sni_healthy_threshold - interval = var.sni_health_interval - interval_jitter = var.sni_health_interval_jitter - timeout = var.sni_health_timeout - unhealthy_threshold = var.sni_unhealthy_threshold - } - session_persistence = { - use_source_ip_address = var.sni_use_source_ip_address - } - }, - { - name = var.udp_target_pool_name - target_port = var.udp_target_port - targets = [ - { - display_name = var.target_display_name - ip = stackit_network_interface.network_interface.ipv4 - } - ] - } - ] - listeners = [ - { - display_name = var.sni_listener_display_name - port = var.sni_listener_port - protocol = var.sni_listener_protocol - target_pool = var.sni_target_pool_name - server_name_indicators = [ - { - name = var.sni_listener_server_name_indicators - } - ] - tcp = { - idle_timeout = var.sni_idle_timeout - } - }, - { - display_name = var.udp_listener_display_name - port = var.udp_listener_port - protocol = var.udp_listener_protocol - target_pool = var.udp_target_pool_name - udp = { - idle_timeout = var.udp_idle_timeout - } - } - ] - networks = [ - { - network_id = stackit_network.network.network_id - role = var.network_role - } - ] - options = { - private_network_only = var.private_network_only - acl = [var.acl] - observability = { - logs = { - credentials_ref = stackit_loadbalancer_observability_credential.logs.credentials_ref - push_url = var.observability_logs_push_url - } - metrics = { - credentials_ref = stackit_loadbalancer_observability_credential.metrics.credentials_ref - push_url = var.observability_metrics_push_url - } - } - } - external_address = stackit_public_ip.public_ip.ip -} - -resource "stackit_loadbalancer_observability_credential" "logs" { - project_id = var.project_id - display_name = var.observability_credential_logs_name - username = var.observability_credential_logs_username - password = var.observability_credential_logs_password -} - -resource "stackit_loadbalancer_observability_credential" "metrics" { - project_id = var.project_id - display_name = var.observability_credential_metrics_name - username = var.observability_credential_metrics_username - password = var.observability_credential_metrics_password -} - diff --git a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf b/stackit/internal/services/loadbalancer/testfiles/resource-min.tf deleted file mode 100644 index f2692c35..00000000 --- a/stackit/internal/services/loadbalancer/testfiles/resource-min.tf +++ /dev/null @@ -1,98 +0,0 @@ - -variable "project_id" {} -variable "network_name" {} -variable "server_name" {} - -variable "loadbalancer_name" {} -variable "target_pool_name" {} -variable "target_port" {} -variable "target_display_name" {} -variable "listener_port" {} -variable "listener_protocol" {} -variable "network_role" {} - -variable "obs_display_name" {} -variable "obs_username" {} -variable "obs_password" {} - -resource "stackit_network" "network" { - project_id = var.project_id - name = var.network_name - ipv4_nameservers = ["8.8.8.8"] - ipv4_prefix = "192.168.2.0/25" - routed = "true" -} - -resource "stackit_network_interface" "network_interface" { - project_id = stackit_network.network.project_id - network_id = stackit_network.network.network_id - name = "name" - lifecycle { - ignore_changes = [ - security_group_ids, - ] - } -} - -resource "stackit_public_ip" "public_ip" { - project_id = var.project_id - network_interface_id = stackit_network_interface.network_interface.network_interface_id - lifecycle { - ignore_changes = [ - network_interface_id - ] - } -} - -resource "stackit_server" "server" { - project_id = var.project_id - availability_zone = "eu01-1" - name = var.server_name - machine_type = "t1.1" - boot_volume = { - size = 32 - source_type = "image" - source_id = "59838a89-51b1-4892-b57f-b3caf598ee2f" - delete_on_termination = "true" - } - network_interfaces = [stackit_network_interface.network_interface.network_interface_id] - user_data = "#!/bin/bash" -} - -resource "stackit_loadbalancer" "loadbalancer" { - project_id = var.project_id - name = var.loadbalancer_name - target_pools = [ - { - name = var.target_pool_name - target_port = var.target_port - targets = [ - { - display_name = var.target_display_name - ip = stackit_network_interface.network_interface.ipv4 - } - ] - } - ] - listeners = [ - { - port = var.listener_port - protocol = var.listener_protocol - target_pool = var.target_pool_name - } - ] - networks = [ - { - network_id = stackit_network.network.network_id - role = var.network_role - } - ] - external_address = stackit_public_ip.public_ip.ip -} - -resource "stackit_loadbalancer_observability_credential" "obs_credential" { - project_id = var.project_id - display_name = var.obs_display_name - username = var.obs_username - password = var.obs_password -} diff --git a/stackit/internal/services/loadbalancer/utils/util.go b/stackit/internal/services/loadbalancer/utils/util.go deleted file mode 100644 index 2b84c566..00000000 --- a/stackit/internal/services/loadbalancer/utils/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *loadbalancer.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.LoadBalancerCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.LoadBalancerCustomEndpoint)) - } - apiClient, err := loadbalancer.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/loadbalancer/utils/util_test.go b/stackit/internal/services/loadbalancer/utils/util_test.go deleted file mode 100644 index b7e118f3..00000000 --- a/stackit/internal/services/loadbalancer/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://loadbalancer-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *loadbalancer.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *loadbalancer.APIClient { - apiClient, err := loadbalancer.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - LoadBalancerCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *loadbalancer.APIClient { - apiClient, err := loadbalancer.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/logme/credential/datasource.go b/stackit/internal/services/logme/credential/datasource.go deleted file mode 100644 index 450845fe..00000000 --- a/stackit/internal/services/logme/credential/datasource.go +++ /dev/null @@ -1,169 +0,0 @@ -package logme - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/logme" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &credentialDataSource{} -) - -// NewCredentialDataSource is a helper function to simplify the provider implementation. -func NewCredentialDataSource() datasource.DataSource { - return &credentialDataSource{} -} - -// credentialDataSource is the data source implementation. -type credentialDataSource struct { - client *logme.APIClient -} - -// Metadata returns the data source type name. -func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_logme_credential" -} - -// Configure adds the provider configured client to the data source. -func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "LogMe credential client configured") -} - -// Schema defines the schema for the data source. -func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "LogMe credential data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the LogMe instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading credential", - fmt.Sprintf("Credential with ID %q or instance with ID %q does not exist in project %q.", credentialId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "LogMe credential read") -} diff --git a/stackit/internal/services/logme/credential/resource.go b/stackit/internal/services/logme/credential/resource.go deleted file mode 100644 index babd4b97..00000000 --- a/stackit/internal/services/logme/credential/resource.go +++ /dev/null @@ -1,343 +0,0 @@ -package logme - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialResource{} - _ resource.ResourceWithConfigure = &credentialResource{} - _ resource.ResourceWithImportState = &credentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialId types.String `tfsdk:"credential_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Host types.String `tfsdk:"host"` - Password types.String `tfsdk:"password"` - Port types.Int64 `tfsdk:"port"` - Uri types.String `tfsdk:"uri"` - Username types.String `tfsdk:"username"` -} - -// NewCredentialResource is a helper function to simplify the provider implementation. -func NewCredentialResource() resource.Resource { - return &credentialResource{} -} - -// credentialResource is the resource implementation. -type credentialResource struct { - client *logme.APIClient -} - -// Metadata returns the resource type name. -func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_logme_credential" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "LogMe credential client configured") -} - -// Schema defines the schema for the resource. -func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "LogMe credential resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the LogMe instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Create new recordset - credentialsResp, err := r.client.CreateCredentials(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if credentialsResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") - return - } - credentialId := *credentialsResp.Id - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "LogMe credential created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "LogMe credential read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - // Delete existing record set - err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "LogMe credential deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id -func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...) - tflog.Info(ctx, "LogMe credential state imported") -} - -func mapFields(credentialsResp *logme.CredentialsResponse, model *Model) error { - if credentialsResp == nil { - return fmt.Errorf("response input is nil") - } - if credentialsResp.Raw == nil { - return fmt.Errorf("response credentials raw is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - credentials := credentialsResp.Raw.Credentials - - var credentialId string - if model.CredentialId.ValueString() != "" { - credentialId = model.CredentialId.ValueString() - } else if credentialsResp.Id != nil { - credentialId = *credentialsResp.Id - } else { - return fmt.Errorf("credentials id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId) - model.CredentialId = types.StringValue(credentialId) - if credentials != nil { - model.Host = types.StringPointerValue(credentials.Host) - model.Password = types.StringPointerValue(credentials.Password) - model.Port = types.Int64PointerValue(credentials.Port) - model.Uri = types.StringPointerValue(credentials.Uri) - model.Username = types.StringPointerValue(credentials.Username) - } - return nil -} diff --git a/stackit/internal/services/logme/credential/resource_test.go b/stackit/internal/services/logme/credential/resource_test.go deleted file mode 100644 index ce1b51de..00000000 --- a/stackit/internal/services/logme/credential/resource_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package logme - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/logme" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *logme.CredentialsResponse - expected Model - isValid bool - }{ - { - "default_values", - &logme.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &logme.RawCredentials{}, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringNull(), - Password: types.StringNull(), - Port: types.Int64Null(), - Uri: types.StringNull(), - Username: types.StringNull(), - }, - true, - }, - { - "simple_values", - &logme.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &logme.RawCredentials{ - Credentials: &logme.Credentials{ - Host: utils.Ptr("host"), - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Username: types.StringValue("username"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &logme.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &logme.RawCredentials{ - Credentials: &logme.Credentials{ - Host: utils.Ptr(""), - Password: utils.Ptr(""), - Port: utils.Ptr(int64(2123456789)), - Uri: nil, - Username: utils.Ptr(""), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue(""), - Password: types.StringValue(""), - Port: types.Int64Value(2123456789), - Uri: types.StringNull(), - Username: types.StringValue(""), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &logme.CredentialsResponse{}, - Model{}, - false, - }, - { - "nil_raw_credential", - &logme.CredentialsResponse{ - Id: utils.Ptr("cid"), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, model) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/logme/instance/datasource.go b/stackit/internal/services/logme/instance/datasource.go deleted file mode 100644 index 7ad2ef2b..00000000 --- a/stackit/internal/services/logme/instance/datasource.go +++ /dev/null @@ -1,296 +0,0 @@ -package logme - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/logme" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *logme.APIClient -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_logme_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "LogMe instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "LogMe instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the LogMe instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "enable_monitoring": "Enable monitoring.", - "graphite": "If set, monitoring with Graphite will be enabled. Expects the host and port where the Graphite metrics should be sent to (host:port).", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted (in seconds).", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "java_heapspace": "The amount of memory (in MB) allocated as heap by the JVM for OpenSearch.", - "java_maxmetaspace": "The amount of memory (in MB) used by the JVM to store metadata for OpenSearch.", - "ism_deletion_after": "Combination of an integer and a timerange when an index will be considered \"old\" and can be deleted. Possible values for the timerange are `s`, `m`, `h` and `d`.", - "ism_job_interval": "Jitter of the execution time.", - "syslog": "List of syslog servers to send logs to.", - "opensearch-tls-ciphers": "List of ciphers to use for TLS.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Computed: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Computed: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Computed: true, - }, - "fluentd_tcp": schema.Int64Attribute{ - Description: parametersDescriptions["fluentd_tcp"], - Computed: true, - }, - "fluentd_tls": schema.Int64Attribute{ - Description: parametersDescriptions["fluentd_tls"], - Computed: true, - }, - "fluentd_tls_ciphers": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_ciphers"], - Computed: true, - }, - "fluentd_tls_max_version": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_max_version"], - Computed: true, - }, - "fluentd_tls_min_version": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_min_version"], - Computed: true, - }, - "fluentd_tls_version": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_version"], - Computed: true, - }, - "fluentd_udp": schema.Int64Attribute{ - Description: parametersDescriptions["fluentd_udp"], - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Computed: true, - }, - "ism_deletion_after": schema.StringAttribute{ - Description: parametersDescriptions["ism_deletion_after"], - Computed: true, - }, - "ism_jitter": schema.Float64Attribute{ - Description: parametersDescriptions["ism_jitter"], - Computed: true, - }, - "ism_job_interval": schema.Int64Attribute{ - Description: parametersDescriptions["ism_job_interval"], - Computed: true, - }, - "java_heapspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_heapspace"], - Computed: true, - }, - "java_maxmetaspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_maxmetaspace"], - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Computed: true, - }, - "opensearch_tls_ciphers": schema.ListAttribute{ - Description: parametersDescriptions["opensearch_tls_ciphers"], - ElementType: types.StringType, - Computed: true, - }, - "opensearch_tls_protocols": schema.ListAttribute{ - Description: parametersDescriptions["opensearch_tls_protocols"], - ElementType: types.StringType, - Computed: true, - }, - "syslog": schema.ListAttribute{ - Description: parametersDescriptions["syslog"], - ElementType: types.StringType, - Computed: true, - }, - }, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - }, - "image_url": schema.StringAttribute{ - Computed: true, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - http.StatusGone: fmt.Sprintf("Instance %q is gone.", instanceId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "LogMe instance read") -} diff --git a/stackit/internal/services/logme/instance/resource.go b/stackit/internal/services/logme/instance/resource.go deleted file mode 100644 index ffcd0fb8..00000000 --- a/stackit/internal/services/logme/instance/resource.go +++ /dev/null @@ -1,928 +0,0 @@ -package logme - -import ( - "context" - "fmt" - "net/http" - "slices" - "strings" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - CfGuid types.String `tfsdk:"cf_guid"` - CfSpaceGuid types.String `tfsdk:"cf_space_guid"` - DashboardUrl types.String `tfsdk:"dashboard_url"` - ImageUrl types.String `tfsdk:"image_url"` - Name types.String `tfsdk:"name"` - CfOrganizationGuid types.String `tfsdk:"cf_organization_guid"` - Parameters types.Object `tfsdk:"parameters"` - Version types.String `tfsdk:"version"` - PlanName types.String `tfsdk:"plan_name"` - PlanId types.String `tfsdk:"plan_id"` -} - -// Struct corresponding to DataSourceModel.Parameters -type parametersModel struct { - SgwAcl types.String `tfsdk:"sgw_acl"` - EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` - FluentdTcp types.Int64 `tfsdk:"fluentd_tcp"` - FluentdTls types.Int64 `tfsdk:"fluentd_tls"` - FluentdTlsCiphers types.String `tfsdk:"fluentd_tls_ciphers"` - FluentdTlsMaxVersion types.String `tfsdk:"fluentd_tls_max_version"` - FluentdTlsMinVersion types.String `tfsdk:"fluentd_tls_min_version"` - FluentdTlsVersion types.String `tfsdk:"fluentd_tls_version"` - FluentdUdp types.Int64 `tfsdk:"fluentd_udp"` - Graphite types.String `tfsdk:"graphite"` - IsmDeletionAfter types.String `tfsdk:"ism_deletion_after"` - IsmJitter types.Float64 `tfsdk:"ism_jitter"` - IsmJobInterval types.Int64 `tfsdk:"ism_job_interval"` - JavaHeapspace types.Int64 `tfsdk:"java_heapspace"` - JavaMaxmetaspace types.Int64 `tfsdk:"java_maxmetaspace"` - MaxDiskThreshold types.Int64 `tfsdk:"max_disk_threshold"` - MetricsFrequency types.Int64 `tfsdk:"metrics_frequency"` - MetricsPrefix types.String `tfsdk:"metrics_prefix"` - MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"` - OpensearchTlsCiphers types.List `tfsdk:"opensearch_tls_ciphers"` - OpensearchTlsProtocols types.List `tfsdk:"opensearch_tls_protocols"` - Syslog types.List `tfsdk:"syslog"` -} - -// Types corresponding to parametersModel -var parametersTypes = map[string]attr.Type{ - "sgw_acl": basetypes.StringType{}, - "enable_monitoring": basetypes.BoolType{}, - "fluentd_tcp": basetypes.Int64Type{}, - "fluentd_tls": basetypes.Int64Type{}, - "fluentd_tls_ciphers": basetypes.StringType{}, - "fluentd_tls_max_version": basetypes.StringType{}, - "fluentd_tls_min_version": basetypes.StringType{}, - "fluentd_tls_version": basetypes.StringType{}, - "fluentd_udp": basetypes.Int64Type{}, - "graphite": basetypes.StringType{}, - "ism_deletion_after": basetypes.StringType{}, - "ism_jitter": basetypes.Float64Type{}, - "ism_job_interval": basetypes.Int64Type{}, - "java_heapspace": basetypes.Int64Type{}, - "java_maxmetaspace": basetypes.Int64Type{}, - "max_disk_threshold": basetypes.Int64Type{}, - "metrics_frequency": basetypes.Int64Type{}, - "metrics_prefix": basetypes.StringType{}, - "monitoring_instance_id": basetypes.StringType{}, - "opensearch_tls_ciphers": basetypes.ListType{ElemType: types.StringType}, - "opensearch_tls_protocols": basetypes.ListType{ElemType: types.StringType}, - "syslog": basetypes.ListType{ElemType: types.StringType}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *logme.APIClient -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_logme_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "LogMe instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "LogMe instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the LogMe instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - "parameters": "Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "enable_monitoring": "Enable monitoring.", - "graphite": "If set, monitoring with Graphite will be enabled. Expects the host and port where the Graphite metrics should be sent to (host:port).", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted (in seconds).", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "java_heapspace": "The amount of memory (in MB) allocated as heap by the JVM for OpenSearch.", - "java_maxmetaspace": "The amount of memory (in MB) used by the JVM to store metadata for OpenSearch.", - "ism_deletion_after": "Combination of an integer and a timerange when an index will be considered \"old\" and can be deleted. Possible values for the timerange are `s`, `m`, `h` and `d`.", - "ism_job_interval": "Jitter of the execution time.", - "syslog": "List of syslog servers to send logs to.", - "opensearch-tls-ciphers": "List of ciphers to use for TLS.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Required: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Required: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Description: descriptions["parameters"], - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Optional: true, - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Optional: true, - Computed: true, - }, - "fluentd_tcp": schema.Int64Attribute{ - Description: parametersDescriptions["fluentd_tcp"], - Optional: true, - Computed: true, - }, - "fluentd_tls": schema.Int64Attribute{ - Description: parametersDescriptions["fluentd_tls"], - Optional: true, - Computed: true, - }, - "fluentd_tls_ciphers": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_ciphers"], - Optional: true, - Computed: true, - }, - "fluentd_tls_max_version": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_max_version"], - Optional: true, - Computed: true, - }, - "fluentd_tls_min_version": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_min_version"], - Optional: true, - Computed: true, - }, - "fluentd_tls_version": schema.StringAttribute{ - Description: parametersDescriptions["fluentd_tls_version"], - Optional: true, - Computed: true, - }, - "fluentd_udp": schema.Int64Attribute{ - Description: parametersDescriptions["fluentd_udp"], - Optional: true, - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Optional: true, - Computed: true, - }, - "ism_deletion_after": schema.StringAttribute{ - Description: parametersDescriptions["ism_deletion_after"], - Optional: true, - Computed: true, - }, - "ism_jitter": schema.Float64Attribute{ - Description: parametersDescriptions["ism_jitter"], - Optional: true, - Computed: true, - }, - "ism_job_interval": schema.Int64Attribute{ - Description: parametersDescriptions["ism_job_interval"], - Optional: true, - Computed: true, - }, - "java_heapspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_heapspace"], - Optional: true, - Computed: true, - }, - "java_maxmetaspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_maxmetaspace"], - Optional: true, - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Optional: true, - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Optional: true, - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Optional: true, - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "opensearch_tls_ciphers": schema.ListAttribute{ - Description: parametersDescriptions["opensearch_tls_ciphers"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "opensearch_tls_protocols": schema.ListAttribute{ - Description: parametersDescriptions["opensearch_tls_protocols"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "syslog": schema.ListAttribute{ - Description: parametersDescriptions["syslog"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - }, - Optional: true, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "image_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.InstanceId - ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(90 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "LogMe instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "LogMe instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - err = r.client.PartialUpdateInstance(ctx, projectId, instanceId).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "LogMe instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "LogMe instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "LogMe instance state imported") -} - -func mapFields(instance *logme.Instance, model *Model) error { - if instance == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.InstanceId != nil { - instanceId = *instance.InstanceId - } else { - return fmt.Errorf("instance id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) - model.InstanceId = types.StringValue(instanceId) - model.PlanId = types.StringPointerValue(instance.PlanId) - model.CfGuid = types.StringPointerValue(instance.CfGuid) - model.CfSpaceGuid = types.StringPointerValue(instance.CfSpaceGuid) - model.DashboardUrl = types.StringPointerValue(instance.DashboardUrl) - model.ImageUrl = types.StringPointerValue(instance.ImageUrl) - model.Name = types.StringPointerValue(instance.Name) - model.CfOrganizationGuid = types.StringPointerValue(instance.CfOrganizationGuid) - - if instance.Parameters == nil { - model.Parameters = types.ObjectNull(parametersTypes) - } else { - parameters, err := mapParameters(*instance.Parameters) - if err != nil { - return fmt.Errorf("mapping parameters: %w", err) - } - model.Parameters = parameters - } - return nil -} - -func mapParameters(params map[string]interface{}) (types.Object, error) { - attributes := map[string]attr.Value{} - for attribute := range parametersTypes { - var valueInterface interface{} - var ok bool - - // This replacement is necessary because Terraform does not allow hyphens in attribute names - // And the API uses hyphens in some of the attribute names, which would cause a mismatch - // The following attributes have hyphens in the API but underscores in the schema - hyphenAttributes := []string{ - "fluentd_tcp", - "fluentd_tls", - "fluentd_tls_ciphers", - "fluentd_tls_max_version", - "fluentd_tls_min_version", - "fluentd_tls_version", - "fluentd_udp", - "opensearch_tls_ciphers", - "opensearch_tls_protocols", - } - if slices.Contains(hyphenAttributes, attribute) { - alteredAttribute := strings.ReplaceAll(attribute, "_", "-") - valueInterface, ok = params[alteredAttribute] - } else { - valueInterface, ok = params[attribute] - } - if !ok { - // All fields are optional, so this is ok - // Set the value as nil, will be handled accordingly - valueInterface = nil - } - - var value attr.Value - switch parametersTypes[attribute].(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) - case basetypes.StringType: - if valueInterface == nil { - value = types.StringNull() - } else { - valueString, ok := valueInterface.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) - } - value = types.StringValue(valueString) - } - case basetypes.BoolType: - if valueInterface == nil { - value = types.BoolNull() - } else { - valueBool, ok := valueInterface.(bool) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) - } - value = types.BoolValue(valueBool) - } - case basetypes.Int64Type: - if valueInterface == nil { - value = types.Int64Null() - } else { - // This may be int64, int32, int or float64 - // We try to assert all 4 - var valueInt64 int64 - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) - case int64: - valueInt64 = temp - case int32: - valueInt64 = int64(temp) - case int: - valueInt64 = int64(temp) - case float64: - valueInt64 = int64(temp) - } - value = types.Int64Value(valueInt64) - } - case basetypes.Float64Type: - if valueInterface == nil { - value = types.Float64Null() - } else { - var valueFloat64 float64 - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) - case float64: - valueFloat64 = float64(temp) - } - value = types.Float64Value(valueFloat64) - } - case basetypes.ListType: // Assumed to be a list of strings - if valueInterface == nil { - value = types.ListNull(types.StringType) - } else { - // This may be []string{} or []interface{} - // We try to assert all 2 - var valueList []attr.Value - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) - case []string: - for _, x := range temp { - valueList = append(valueList, types.StringValue(x)) - } - case []interface{}: - for _, x := range temp { - xString, ok := x.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) - } - valueList = append(valueList, types.StringValue(xString)) - } - } - temp2, diags := types.ListValue(types.StringType, valueList) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) - } - value = temp2 - } - } - attributes[attribute] = value - } - - output, diags := types.ObjectValue(parametersTypes, attributes) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) - } - return output, nil -} - -func toCreatePayload(model *Model, parameters *parametersModel) (*logme.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("convert parameters: %w", err) - } - - return &logme.CreateInstancePayload{ - InstanceName: conversion.StringValueToPointer(model.Name), - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toUpdatePayload(model *Model, parameters *parametersModel) (*logme.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("convert parameters: %w", err) - } - - return &logme.PartialUpdateInstancePayload{ - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toInstanceParams(parameters *parametersModel) (*logme.InstanceParameters, error) { - if parameters == nil { - return nil, nil - } - payloadParams := &logme.InstanceParameters{} - - payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) - payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring) - payloadParams.FluentdTcp = conversion.Int64ValueToPointer(parameters.FluentdTcp) - payloadParams.FluentdTls = conversion.Int64ValueToPointer(parameters.FluentdTls) - payloadParams.FluentdTlsCiphers = conversion.StringValueToPointer(parameters.FluentdTlsCiphers) - payloadParams.FluentdTlsMaxVersion = conversion.StringValueToPointer(parameters.FluentdTlsMaxVersion) - payloadParams.FluentdTlsMinVersion = conversion.StringValueToPointer(parameters.FluentdTlsMinVersion) - payloadParams.FluentdTlsVersion = conversion.StringValueToPointer(parameters.FluentdTlsVersion) - payloadParams.FluentdUdp = conversion.Int64ValueToPointer(parameters.FluentdUdp) - payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite) - payloadParams.IsmDeletionAfter = conversion.StringValueToPointer(parameters.IsmDeletionAfter) - payloadParams.IsmJitter = conversion.Float64ValueToPointer(parameters.IsmJitter) - payloadParams.IsmJobInterval = conversion.Int64ValueToPointer(parameters.IsmJobInterval) - payloadParams.JavaHeapspace = conversion.Int64ValueToPointer(parameters.JavaHeapspace) - payloadParams.JavaMaxmetaspace = conversion.Int64ValueToPointer(parameters.JavaMaxmetaspace) - payloadParams.MaxDiskThreshold = conversion.Int64ValueToPointer(parameters.MaxDiskThreshold) - payloadParams.MetricsFrequency = conversion.Int64ValueToPointer(parameters.MetricsFrequency) - payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix) - payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) - - var err error - payloadParams.OpensearchTlsCiphers, err = conversion.StringListToPointer(parameters.OpensearchTlsCiphers) - if err != nil { - return nil, fmt.Errorf("convert opensearch_tls_ciphers: %w", err) - } - - payloadParams.OpensearchTlsProtocols, err = conversion.StringListToPointer(parameters.OpensearchTlsProtocols) - if err != nil { - return nil, fmt.Errorf("convert opensearch_tls_protocols: %w", err) - } - - payloadParams.Syslog, err = conversion.StringListToPointer(parameters.Syslog) - if err != nil { - return nil, fmt.Errorf("convert syslog: %w", err) - } - - return payloadParams, nil -} - -func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { - projectId := model.ProjectId.ValueString() - res, err := r.client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting LogMe offerings: %w", err) - } - - version := model.Version.ValueString() - planName := model.PlanName.ValueString() - availableVersions := "" - availablePlanNames := "" - isValidVersion := false - for _, offer := range *res.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) - continue - } - isValidVersion = true - - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue - } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - model.PlanId = types.StringPointerValue(plan.Id) - return nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) - } - } - - if !isValidVersion { - return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) - } - return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) -} - -func loadPlanNameAndVersion(ctx context.Context, client *logme.APIClient, model *Model) error { - projectId := model.ProjectId.ValueString() - planId := model.PlanId.ValueString() - res, err := client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting LogMe offerings: %w", err) - } - - for _, offer := range *res.Offerings { - for _, plan := range *offer.Plans { - if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { - model.PlanName = types.StringPointerValue(plan.Name) - model.Version = types.StringPointerValue(offer.Version) - return nil - } - } - } - - return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) -} diff --git a/stackit/internal/services/logme/instance/resource_test.go b/stackit/internal/services/logme/instance/resource_test.go deleted file mode 100644 index d74d81d1..00000000 --- a/stackit/internal/services/logme/instance/resource_test.go +++ /dev/null @@ -1,402 +0,0 @@ -package logme - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/logme" -) - -var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringValue("acl"), - "enable_monitoring": types.BoolValue(true), - "fluentd_tcp": types.Int64Value(10), - "fluentd_tls": types.Int64Value(10), - "fluentd_tls_ciphers": types.StringValue("ciphers"), - "fluentd_tls_max_version": types.StringValue("max_version"), - "fluentd_tls_min_version": types.StringValue("min_version"), - "fluentd_tls_version": types.StringValue("version"), - "fluentd_udp": types.Int64Value(10), - "graphite": types.StringValue("graphite"), - "ism_deletion_after": types.StringValue("deletion_after"), - "ism_jitter": types.Float64Value(10.1), - "ism_job_interval": types.Int64Value(10), - "java_heapspace": types.Int64Value(10), - "java_maxmetaspace": types.Int64Value(10), - "max_disk_threshold": types.Int64Value(10), - "metrics_frequency": types.Int64Value(10), - "metrics_prefix": types.StringValue("prefix"), - "monitoring_instance_id": types.StringValue("mid"), - "opensearch_tls_ciphers": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ciphers"), - types.StringValue("ciphers2"), - }), - "opensearch_tls_protocols": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("protocols"), - types.StringValue("protocols2"), - }), - "syslog": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("syslog"), - types.StringValue("syslog2"), - }), -}) - -var fixtureNullModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringNull(), - "enable_monitoring": types.BoolNull(), - "fluentd_tcp": types.Int64Null(), - "fluentd_tls": types.Int64Null(), - "fluentd_tls_ciphers": types.StringNull(), - "fluentd_tls_max_version": types.StringNull(), - "fluentd_tls_min_version": types.StringNull(), - "fluentd_tls_version": types.StringNull(), - "fluentd_udp": types.Int64Null(), - "graphite": types.StringNull(), - "ism_deletion_after": types.StringNull(), - "ism_jitter": types.Float64Null(), - "ism_job_interval": types.Int64Null(), - "java_heapspace": types.Int64Null(), - "java_maxmetaspace": types.Int64Null(), - "max_disk_threshold": types.Int64Null(), - "metrics_frequency": types.Int64Null(), - "metrics_prefix": types.StringNull(), - "monitoring_instance_id": types.StringNull(), - "opensearch_tls_ciphers": types.ListNull(types.StringType), - "opensearch_tls_protocols": types.ListNull(types.StringType), - "syslog": types.ListNull(types.StringType), -}) - -var fixtureInstanceParameters = logme.InstanceParameters{ - SgwAcl: utils.Ptr("acl"), - EnableMonitoring: utils.Ptr(true), - FluentdTcp: utils.Ptr(int64(10)), - FluentdTls: utils.Ptr(int64(10)), - FluentdTlsCiphers: utils.Ptr("ciphers"), - FluentdTlsMaxVersion: utils.Ptr("max_version"), - FluentdTlsMinVersion: utils.Ptr("min_version"), - FluentdTlsVersion: utils.Ptr("version"), - FluentdUdp: utils.Ptr(int64(10)), - Graphite: utils.Ptr("graphite"), - IsmDeletionAfter: utils.Ptr("deletion_after"), - IsmJitter: utils.Ptr(10.1), - IsmJobInterval: utils.Ptr(int64(10)), - JavaHeapspace: utils.Ptr(int64(10)), - JavaMaxmetaspace: utils.Ptr(int64(10)), - MaxDiskThreshold: utils.Ptr(int64(10)), - MetricsFrequency: utils.Ptr(int64(10)), - MetricsPrefix: utils.Ptr("prefix"), - MonitoringInstanceId: utils.Ptr("mid"), - OpensearchTlsCiphers: &[]string{"ciphers", "ciphers2"}, - OpensearchTlsProtocols: &[]string{"protocols", "protocols2"}, - Syslog: &[]string{"syslog", "syslog2"}, -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *logme.Instance - expected Model - isValid bool - }{ - { - "default_values", - &logme.Instance{}, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringNull(), - Name: types.StringNull(), - CfGuid: types.StringNull(), - CfSpaceGuid: types.StringNull(), - DashboardUrl: types.StringNull(), - ImageUrl: types.StringNull(), - CfOrganizationGuid: types.StringNull(), - Parameters: types.ObjectNull(parametersTypes), - }, - true, - }, - { - "simple_values", - &logme.Instance{ - PlanId: utils.Ptr("plan"), - CfGuid: utils.Ptr("cf"), - CfSpaceGuid: utils.Ptr("space"), - DashboardUrl: utils.Ptr("dashboard"), - ImageUrl: utils.Ptr("image"), - InstanceId: utils.Ptr("iid"), - Name: utils.Ptr("name"), - CfOrganizationGuid: utils.Ptr("org"), - Parameters: &map[string]interface{}{ - // Using "-" on purpose on some fields because that is the API response - "sgw_acl": "acl", - "enable_monitoring": true, - "fluentd-tcp": 10, - "fluentd-tls": 10, - "fluentd-tls-ciphers": "ciphers", - "fluentd-tls-max-version": "max_version", - "fluentd-tls-min-version": "min_version", - "fluentd-tls-version": "version", - "fluentd-udp": 10, - "graphite": "graphite", - "ism_deletion_after": "deletion_after", - "ism_jitter": 10.1, - "ism_job_interval": 10, - "java_heapspace": 10, - "java_maxmetaspace": 10, - "max_disk_threshold": 10, - "metrics_frequency": 10, - "metrics_prefix": "prefix", - "monitoring_instance_id": "mid", - "opensearch-tls-ciphers": []string{"ciphers", "ciphers2"}, - "opensearch-tls-protocols": []string{"protocols", "protocols2"}, - "syslog": []string{"syslog", "syslog2"}, - }, - }, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringValue("plan"), - Name: types.StringValue("name"), - CfGuid: types.StringValue("cf"), - CfSpaceGuid: types.StringValue("space"), - DashboardUrl: types.StringValue("dashboard"), - ImageUrl: types.StringValue("image"), - CfOrganizationGuid: types.StringValue("org"), - Parameters: fixtureModelParameters, - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &logme.Instance{}, - Model{}, - false, - }, - { - "wrong_param_types_1", - &logme.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": true, - }, - }, - Model{}, - false, - }, - { - "wrong_param_types_2", - &logme.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": 1, - }, - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *logme.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &logme.CreateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &logme.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - PlanId: utils.Ptr("plan"), - Parameters: &fixtureInstanceParameters, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - PlanId: types.StringValue(""), - Parameters: fixtureNullModelParameters, - }, - &logme.CreateInstancePayload{ - InstanceName: utils.Ptr(""), - PlanId: utils.Ptr(""), - Parameters: &logme.InstanceParameters{}, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - }, - &logme.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toCreatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *logme.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &logme.PartialUpdateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &logme.PartialUpdateInstancePayload{ - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - PlanId: types.StringValue(""), - Parameters: fixtureNullModelParameters, - }, - &logme.PartialUpdateInstancePayload{ - Parameters: &logme.InstanceParameters{}, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - PlanId: types.StringValue("plan"), - }, - &logme.PartialUpdateInstancePayload{ - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toUpdatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/logme/logme_acc_test.go b/stackit/internal/services/logme/logme_acc_test.go deleted file mode 100644 index 03cb3b4f..00000000 --- a/stackit/internal/services/logme/logme_acc_test.go +++ /dev/null @@ -1,489 +0,0 @@ -package logme_test - -import ( - "context" - _ "embed" - "fmt" - "maps" - "strings" - "testing" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - core_config "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/resource-min.tf - resourceMinConfig string - - //go:embed testdata/resource-max.tf - resourceMaxConfig string -) - -var ( - minTestName = testutil.ResourceNameWithDateTime("logme-min") - maxTestName = testutil.ResourceNameWithDateTime("logme-max") -) - -// Instance resource data -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(minTestName), - "plan_id": config.StringVariable("7a54492c-8a2e-4d3c-b6c2-a4f20cb65912"), // stackit-logme2-1.4.10-single - "plan_name": config.StringVariable("stackit-logme2-1.4.10-single"), - "logme_version": config.StringVariable("2"), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(maxTestName), - "plan_id": config.StringVariable("7a54492c-8a2e-4d3c-b6c2-a4f20cb65912"), // stackit-logme2-1.4.10-single - "logme_version": config.StringVariable("2"), - - "plan_name": config.StringVariable("stackit-logme2-1.4.10-single"), - "params_enable_monitoring": config.BoolVariable(false), - "params_fluentd_tcp": config.IntegerVariable(4), - "params_fluentd_tls": config.IntegerVariable(1), - "params_fluentd_tls_ciphers": config.StringVariable("ALL:!aNULL:!eNULL:!SSLv2"), - "params_fluentd_tls_max_version": config.StringVariable("TLS1_3"), - "params_fluentd_tls_min_version": config.StringVariable("TLS1_1"), - "params_fluentd_tls_version": config.StringVariable("TLS1_2"), - "params_fluentd_udp": config.IntegerVariable(1234), - "params_graphite": config.StringVariable("graphite.example.com:12345"), - "params_ism_deletion_after": config.StringVariable("30d"), - "params_ism_jitter": config.FloatVariable(0.6), - "params_ism_job_interval": config.IntegerVariable(5), - "params_java_heapspace": config.IntegerVariable(256), - "params_java_maxmetaspace": config.IntegerVariable(512), - "params_max_disk_threshold": config.IntegerVariable(80), - "params_metrics_frequency": config.IntegerVariable(10), - "params_metrics_prefix": config.StringVariable("actest"), - "params_monitoring_instance_id": config.StringVariable(uuid.NewString()), - "params_opensearch_tls_ciphers": config.StringVariable("TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA256"), - "params_opensearch_tls_cipher1": config.StringVariable("TLS_DHE_RSA_WITH_AES_256_CBC_SHA"), - "params_opensearch_tls_cipher2": config.StringVariable("TLS_DHE_DSS_WITH_AES_128_CBC_SHA256"), - "params_opensearch_tls_protocol1": config.StringVariable("TLSv1.2"), - "params_opensearch_tls_protocol2": config.StringVariable("TLSv1.3"), - "params_sgw_acl": config.StringVariable("192.168.0.0/16,192.168.0.0/24"), - "params_syslog1": config.StringVariable("syslog1.example.com:514"), - "params_syslog2": config.StringVariable("syslog2.example.com:514"), -} - -func configVarsMinUpdated() config.Variables { - updatedConfig := maps.Clone(testConfigVarsMax) - updatedConfig["name"] = config.StringVariable(minTestName + "-updated") - return updatedConfig -} - -func configVarsMaxUpdated() config.Variables { - updatedConfig := maps.Clone(testConfigVarsMax) - updatedConfig["parameters_max_disk_threshold"] = config.IntegerVariable(85) - updatedConfig["parameters_metrics_frequency"] = config.IntegerVariable(10) - updatedConfig["parameters_graphite"] = config.StringVariable("graphite.stackit.cloud:2003") - updatedConfig["parameters_sgw_acl"] = config.StringVariable("192.168.1.0/24") - updatedConfig["parameters_syslog"] = config.StringVariable("test.log:514") - return updatedConfig -} - -func TestAccLogMeMinResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckLogMeDestroy, - Steps: []resource.TestStep{ - - // Creation - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMin["plan_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMin["plan_name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMin["logme_version"])), - - // Credential data - resource.TestCheckResourceAttrPair( - "stackit_logme_credential.credential", "project_id", - "stackit_logme_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_logme_credential.credential", "instance_id", - "stackit_logme_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_logme_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_logme_credential.credential", "host"), - ), - }, - // Data source - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - - resource.TestCheckResourceAttrPair( - "stackit_logme_instance.instance", "instance_id", - "data.stackit_logme_instance.instance", "instance_id", - ), - - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "cf_guid"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "cf_organization_guid"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "cf_space_guid"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "id"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "image_url"), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMin["plan_id"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMin["plan_name"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMin["logme_version"])), - - // Credential data - resource.TestCheckResourceAttr("data.stackit_logme_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "password"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "host"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "port"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "uri"), - ), - }, - // Import - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_logme_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_logme_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_logme_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "stackit_logme_credential.credential", - ConfigVariables: testConfigVarsMin, - - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_logme_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_logme_credential.credential") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: configVarsMinUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["plan_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "name", testutil.ConvertConfigVariable(configVarsMinUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_name", testutil.ConvertConfigVariable(configVarsMinUpdated()["plan_name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "version", testutil.ConvertConfigVariable(configVarsMinUpdated()["logme_version"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccLogMeMaxResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckLogMeDestroy, - Steps: []resource.TestStep{ - - // Creation - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMax["plan_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMax["plan_name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMax["logme_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.enable_monitoring", testutil.ConvertConfigVariable(testConfigVarsMax["params_enable_monitoring"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tcp", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tcp"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_ciphers", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_ciphers"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_max_version", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_max_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_min_version", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_min_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_version", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_udp", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_udp"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.graphite", testutil.ConvertConfigVariable(testConfigVarsMax["params_graphite"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.ism_deletion_after", testutil.ConvertConfigVariable(testConfigVarsMax["params_ism_deletion_after"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.ism_jitter", testutil.ConvertConfigVariable(testConfigVarsMax["params_ism_jitter"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.ism_job_interval", testutil.ConvertConfigVariable(testConfigVarsMax["params_ism_job_interval"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.java_heapspace", testutil.ConvertConfigVariable(testConfigVarsMax["params_java_heapspace"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.java_maxmetaspace", testutil.ConvertConfigVariable(testConfigVarsMax["params_java_maxmetaspace"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.max_disk_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["params_max_disk_threshold"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.metrics_frequency", testutil.ConvertConfigVariable(testConfigVarsMax["params_metrics_frequency"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.metrics_prefix", testutil.ConvertConfigVariable(testConfigVarsMax["params_metrics_prefix"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.#", "2"), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_cipher1"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_cipher2"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.#", "2"), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_protocol1"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_protocol2"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.sgw_acl", testutil.ConvertConfigVariable(testConfigVarsMax["params_sgw_acl"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.#", "2"), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog1"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog2"])), - - // Credential data - resource.TestCheckResourceAttrPair( - "stackit_logme_credential.credential", "project_id", - "stackit_logme_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_logme_credential.credential", "instance_id", - "stackit_logme_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_logme_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_logme_credential.credential", "host"), - ), - }, - // Data source - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - - resource.TestCheckResourceAttrPair( - "stackit_logme_instance.instance", "instance_id", - "data.stackit_logme_instance.instance", "instance_id", - ), - - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "cf_guid"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "cf_organization_guid"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "cf_space_guid"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "id"), - resource.TestCheckResourceAttrSet("data.stackit_logme_instance.instance", "image_url"), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "plan_id", testutil.ConvertConfigVariable(testConfigVarsMax["plan_id"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMax["plan_name"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMax["logme_version"])), - - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.enable_monitoring", testutil.ConvertConfigVariable(testConfigVarsMax["params_enable_monitoring"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.fluentd_tcp", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tcp"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.fluentd_tls", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.fluentd_tls_ciphers", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_ciphers"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.fluentd_tls_max_version", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_max_version"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.fluentd_tls_min_version", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_min_version"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.fluentd_tls_version", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_tls_version"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.fluentd_udp", testutil.ConvertConfigVariable(testConfigVarsMax["params_fluentd_udp"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.ism_deletion_after", testutil.ConvertConfigVariable(testConfigVarsMax["params_ism_deletion_after"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.ism_jitter", testutil.ConvertConfigVariable(testConfigVarsMax["params_ism_jitter"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.ism_job_interval", testutil.ConvertConfigVariable(testConfigVarsMax["params_ism_job_interval"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.java_heapspace", testutil.ConvertConfigVariable(testConfigVarsMax["params_java_heapspace"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.java_maxmetaspace", testutil.ConvertConfigVariable(testConfigVarsMax["params_java_maxmetaspace"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.max_disk_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["params_max_disk_threshold"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.metrics_frequency", testutil.ConvertConfigVariable(testConfigVarsMax["params_metrics_frequency"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.metrics_prefix", testutil.ConvertConfigVariable(testConfigVarsMax["params_metrics_prefix"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.#", "2"), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_cipher1"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_cipher2"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.#", "2"), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_protocol1"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_opensearch_tls_protocol2"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.sgw_acl", testutil.ConvertConfigVariable(testConfigVarsMax["params_sgw_acl"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.syslog.#", "2"), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog1"])), - resource.TestCheckResourceAttr("data.stackit_logme_instance.instance", "parameters.syslog.1", testutil.ConvertConfigVariable(testConfigVarsMax["params_syslog2"])), - - // Credential data - resource.TestCheckResourceAttr("data.stackit_logme_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "password"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "host"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "port"), - resource.TestCheckResourceAttrSet("data.stackit_logme_credential.credential", "uri"), - ), - }, - // Import - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_logme_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_logme_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_logme_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "stackit_logme_credential.credential", - ConfigVariables: testConfigVarsMax, - - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_logme_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_logme_credential.credential") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: testutil.LogMeProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["plan_id"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "plan_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["plan_name"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "version", testutil.ConvertConfigVariable(configVarsMaxUpdated()["logme_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.enable_monitoring", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_enable_monitoring"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tcp", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_fluentd_tcp"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_fluentd_tls"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_ciphers", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_fluentd_tls_ciphers"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_max_version", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_fluentd_tls_max_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_min_version", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_fluentd_tls_min_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_tls_version", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_fluentd_tls_version"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.fluentd_udp", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_fluentd_udp"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.graphite", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_graphite"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.ism_deletion_after", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_ism_deletion_after"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.ism_jitter", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_ism_jitter"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.ism_job_interval", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_ism_job_interval"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.java_heapspace", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_java_heapspace"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.java_maxmetaspace", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_java_maxmetaspace"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.max_disk_threshold", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_max_disk_threshold"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.metrics_frequency", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_metrics_frequency"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.metrics_prefix", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_metrics_prefix"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.#", "2"), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_opensearch_tls_cipher1"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_ciphers.1", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_opensearch_tls_cipher2"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.#", "2"), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_opensearch_tls_protocol1"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.opensearch_tls_protocols.1", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_opensearch_tls_protocol2"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.sgw_acl", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_sgw_acl"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.#", "2"), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_syslog1"])), - resource.TestCheckResourceAttr("stackit_logme_instance.instance", "parameters.syslog.1", testutil.ConvertConfigVariable(configVarsMaxUpdated()["params_syslog2"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckLogMeDestroy(s *terraform.State) error { - ctx := context.Background() - var client *logme.APIClient - var err error - if testutil.LogMeCustomEndpoint == "" { - client, err = logme.NewAPIClient( - core_config.WithRegion("eu01"), - ) - } else { - client, err = logme.NewAPIClient( - core_config.WithEndpoint(testutil.LogMeCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_logme_instance" { - continue - } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := *instancesResp.Instances - for i := range instances { - if instances[i].InstanceId == nil { - continue - } - if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { - if !checkInstanceDeleteSuccess(&instances[i]) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) - } - } - } - } - return nil -} - -func checkInstanceDeleteSuccess(i *logme.Instance) bool { - if *i.LastOperation.Type != logme.INSTANCELASTOPERATIONTYPE_DELETE { - return false - } - - if *i.LastOperation.Type == logme.INSTANCELASTOPERATIONTYPE_DELETE { - if *i.LastOperation.State != logme.INSTANCELASTOPERATIONSTATE_SUCCEEDED { - return false - } else if strings.Contains(*i.LastOperation.Description, "DeleteFailed") || strings.Contains(*i.LastOperation.Description, "failed") { - return false - } - } - return true -} diff --git a/stackit/internal/services/logme/testdata/resource-max.tf b/stackit/internal/services/logme/testdata/resource-max.tf deleted file mode 100644 index 43e2c5f1..00000000 --- a/stackit/internal/services/logme/testdata/resource-max.tf +++ /dev/null @@ -1,78 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "plan_name" {} -variable "logme_version" {} -variable "params_enable_monitoring" {} -variable "params_fluentd_tcp" {} -variable "params_fluentd_tls" {} -variable "params_fluentd_tls_ciphers" {} -variable "params_fluentd_tls_max_version" {} -variable "params_fluentd_tls_min_version" {} -variable "params_fluentd_tls_version" {} -variable "params_fluentd_udp" {} -variable "params_graphite" {} -variable "params_ism_deletion_after" {} -variable "params_ism_jitter" {} -variable "params_ism_job_interval" {} -variable "params_java_heapspace" {} -variable "params_java_maxmetaspace" {} -variable "params_max_disk_threshold" {} -variable "params_metrics_frequency" {} -variable "params_metrics_prefix" {} -variable "params_monitoring_instance_id" {} -variable "params_opensearch_tls_cipher1" {} -variable "params_opensearch_tls_cipher2" {} -variable "params_opensearch_tls_protocol1" {} -variable "params_opensearch_tls_protocol2" {} -variable "params_sgw_acl" {} -variable "params_syslog1" {} -variable "params_syslog2" {} - -resource "stackit_logme_instance" "instance" { - project_id = var.project_id - name = var.name - plan_name = var.plan_name - version = var.logme_version - - parameters = { - enable_monitoring = var.params_enable_monitoring - fluentd_tcp = var.params_fluentd_tcp - fluentd_tls = var.params_fluentd_tls - fluentd_tls_ciphers = var.params_fluentd_tls_ciphers - fluentd_tls_max_version = var.params_fluentd_tls_max_version - fluentd_tls_min_version = var.params_fluentd_tls_min_version - fluentd_tls_version = var.params_fluentd_tls_version - fluentd_udp = var.params_fluentd_udp - graphite = var.params_graphite - ism_deletion_after = var.params_ism_deletion_after - ism_jitter = var.params_ism_jitter - ism_job_interval = var.params_ism_job_interval - java_heapspace = var.params_java_heapspace - java_maxmetaspace = var.params_java_maxmetaspace - max_disk_threshold = var.params_max_disk_threshold - metrics_frequency = var.params_metrics_frequency - metrics_prefix = var.params_metrics_prefix - opensearch_tls_ciphers = [var.params_opensearch_tls_cipher1, var.params_opensearch_tls_cipher2] - opensearch_tls_protocols = [var.params_opensearch_tls_protocol1, var.params_opensearch_tls_protocol2] - sgw_acl = var.params_sgw_acl - syslog = [var.params_syslog1, var.params_syslog2] - - } -} - -resource "stackit_logme_credential" "credential" { - project_id = stackit_logme_instance.instance.project_id - instance_id = stackit_logme_instance.instance.instance_id -} - - -data "stackit_logme_instance" "instance" { - project_id = stackit_logme_instance.instance.project_id - instance_id = stackit_logme_instance.instance.instance_id -} - -data "stackit_logme_credential" "credential" { - project_id = stackit_logme_credential.credential.project_id - instance_id = stackit_logme_credential.credential.instance_id - credential_id = stackit_logme_credential.credential.credential_id -} diff --git a/stackit/internal/services/logme/testdata/resource-min.tf b/stackit/internal/services/logme/testdata/resource-min.tf deleted file mode 100644 index 8552c77f..00000000 --- a/stackit/internal/services/logme/testdata/resource-min.tf +++ /dev/null @@ -1,28 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "plan_name" {} -variable "logme_version" {} - -resource "stackit_logme_instance" "instance" { - project_id = var.project_id - name = var.name - plan_name = var.plan_name - version = var.logme_version -} - -resource "stackit_logme_credential" "credential" { - project_id = stackit_logme_instance.instance.project_id - instance_id = stackit_logme_instance.instance.instance_id -} - - -data "stackit_logme_instance" "instance" { - project_id = stackit_logme_instance.instance.project_id - instance_id = stackit_logme_instance.instance.instance_id -} - -data "stackit_logme_credential" "credential" { - project_id = stackit_logme_credential.credential.project_id - instance_id = stackit_logme_credential.credential.instance_id - credential_id = stackit_logme_credential.credential.credential_id -} diff --git a/stackit/internal/services/logme/utils/util.go b/stackit/internal/services/logme/utils/util.go deleted file mode 100644 index 6da59478..00000000 --- a/stackit/internal/services/logme/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *logme.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.LogMeCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.LogMeCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := logme.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/logme/utils/util_test.go b/stackit/internal/services/logme/utils/util_test.go deleted file mode 100644 index 9fa90199..00000000 --- a/stackit/internal/services/logme/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://logme-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *logme.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *logme.APIClient { - apiClient, err := logme.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - LogMeCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *logme.APIClient { - apiClient, err := logme.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/mariadb/credential/datasource.go b/stackit/internal/services/mariadb/credential/datasource.go deleted file mode 100644 index b5b37fcf..00000000 --- a/stackit/internal/services/mariadb/credential/datasource.go +++ /dev/null @@ -1,177 +0,0 @@ -package mariadb - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &credentialDataSource{} -) - -// NewCredentialDataSource is a helper function to simplify the provider implementation. -func NewCredentialDataSource() datasource.DataSource { - return &credentialDataSource{} -} - -// credentialDataSource is the data source implementation. -type credentialDataSource struct { - client *mariadb.APIClient -} - -// Metadata returns the data source type name. -func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mariadb_credential" -} - -// Configure adds the provider configured client to the data source. -func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "mariadb credential client configured") -} - -// Schema defines the schema for the data source. -func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "MariaDB credential data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the MariaDB instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "name": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading credential", - fmt.Sprintf("Credential with ID %q or instance with ID %q does not exist in project %q.", credentialId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "mariadb credential read") -} diff --git a/stackit/internal/services/mariadb/credential/resource.go b/stackit/internal/services/mariadb/credential/resource.go deleted file mode 100644 index d106d9d4..00000000 --- a/stackit/internal/services/mariadb/credential/resource.go +++ /dev/null @@ -1,375 +0,0 @@ -package mariadb - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialResource{} - _ resource.ResourceWithConfigure = &credentialResource{} - _ resource.ResourceWithImportState = &credentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialId types.String `tfsdk:"credential_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Host types.String `tfsdk:"host"` - Hosts types.List `tfsdk:"hosts"` - Name types.String `tfsdk:"name"` - Password types.String `tfsdk:"password"` - Port types.Int64 `tfsdk:"port"` - Uri types.String `tfsdk:"uri"` - Username types.String `tfsdk:"username"` -} - -// NewCredentialResource is a helper function to simplify the provider implementation. -func NewCredentialResource() resource.Resource { - return &credentialResource{} -} - -// credentialResource is the resource implementation. -type credentialResource struct { - client *mariadb.APIClient -} - -// Metadata returns the resource type name. -func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mariadb_credential" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "MariaDB credential client configured") -} - -// Schema defines the schema for the resource. -func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "MariaDB credential resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the MariaDB instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "name": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Create new recordset - credentialsResp, err := r.client.CreateCredentials(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if credentialsResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") - return - } - credentialId := *credentialsResp.Id - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MariaDB credential created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MariaDB credential read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - // Delete existing record set - err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "MariaDB credential deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id -func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...) - tflog.Info(ctx, "MariaDB credential state imported") -} - -func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse, model *Model) error { - if credentialsResp == nil { - return fmt.Errorf("response input is nil") - } - if credentialsResp.Raw == nil { - return fmt.Errorf("response credentials raw is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - credentials := credentialsResp.Raw.Credentials - - var credentialId string - if model.CredentialId.ValueString() != "" { - credentialId = model.CredentialId.ValueString() - } else if credentialsResp.Id != nil { - credentialId = *credentialsResp.Id - } else { - return fmt.Errorf("credentials id not present") - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), - model.InstanceId.ValueString(), - credentialId, - ) - - modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) - if err != nil { - return err - } - - model.Hosts = types.ListNull(types.StringType) - model.CredentialId = types.StringValue(credentialId) - if credentials != nil { - if credentials.Hosts != nil { - respHosts := *credentials.Hosts - - reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) - - hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) - if diags.HasError() { - return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) - } - - model.Hosts = hostsTF - } - model.Host = types.StringPointerValue(credentials.Host) - model.Name = types.StringPointerValue(credentials.Name) - model.Password = types.StringPointerValue(credentials.Password) - model.Port = types.Int64PointerValue(credentials.Port) - model.Uri = types.StringPointerValue(credentials.Uri) - model.Username = types.StringPointerValue(credentials.Username) - } - return nil -} diff --git a/stackit/internal/services/mariadb/credential/resource_test.go b/stackit/internal/services/mariadb/credential/resource_test.go deleted file mode 100644 index 911b57f3..00000000 --- a/stackit/internal/services/mariadb/credential/resource_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package mariadb - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *mariadb.CredentialsResponse - expected Model - isValid bool - }{ - { - "default_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &mariadb.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &mariadb.RawCredentials{}, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringNull(), - Hosts: types.ListNull(types.StringType), - Name: types.StringNull(), - Password: types.StringNull(), - Port: types.Int64Null(), - Uri: types.StringNull(), - Username: types.StringNull(), - }, - true, - }, - { - "simple_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &mariadb.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &mariadb.RawCredentials{ - Credentials: &mariadb.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "host_1", - "", - }, - Name: utils.Ptr("name"), - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_1"), - types.StringValue(""), - }), - Name: types.StringValue("name"), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Username: types.StringValue("username"), - }, - true, - }, - { - "hosts_unordered", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - }, - &mariadb.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &mariadb.RawCredentials{ - Credentials: &mariadb.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "", - "host_1", - "host_2", - }, - Name: utils.Ptr("name"), - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - Name: types.StringValue("name"), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Username: types.StringValue("username"), - }, - true, - }, - { - "null_fields_and_int_conversions", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &mariadb.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &mariadb.RawCredentials{ - Credentials: &mariadb.Credentials{ - Host: utils.Ptr(""), - Hosts: &[]string{}, - Name: nil, - Password: utils.Ptr(""), - Port: utils.Ptr(int64(2123456789)), - Uri: nil, - Username: utils.Ptr(""), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue(""), - Hosts: types.ListValueMust(types.StringType, []attr.Value{}), - Name: types.StringNull(), - Password: types.StringValue(""), - Port: types.Int64Value(2123456789), - Uri: types.StringNull(), - Username: types.StringValue(""), - }, - true, - }, - { - "nil_response", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &mariadb.CredentialsResponse{}, - Model{}, - false, - }, - { - "nil_raw_credential", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &mariadb.CredentialsResponse{ - Id: utils.Ptr("cid"), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/mariadb/instance/datasource.go b/stackit/internal/services/mariadb/instance/datasource.go deleted file mode 100644 index 70640000..00000000 --- a/stackit/internal/services/mariadb/instance/datasource.go +++ /dev/null @@ -1,232 +0,0 @@ -package mariadb - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *mariadb.APIClient -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mariadb_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "MariaDB instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "MariaDB instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the MariaDB instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "enable_monitoring": "Enable monitoring.", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted.", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "syslog": "List of syslog servers to send logs to.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Computed: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Computed: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Computed: true, - }, - "syslog": schema.ListAttribute{ - Description: parametersDescriptions["syslog"], - ElementType: types.StringType, - Computed: true, - }, - }, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - }, - "image_url": schema.StringAttribute{ - Computed: true, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - http.StatusGone: fmt.Sprintf("Instance %q is gone.", instanceId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MariaDB instance read") -} diff --git a/stackit/internal/services/mariadb/instance/resource.go b/stackit/internal/services/mariadb/instance/resource.go deleted file mode 100644 index de563b78..00000000 --- a/stackit/internal/services/mariadb/instance/resource.go +++ /dev/null @@ -1,760 +0,0 @@ -package mariadb - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - CfGuid types.String `tfsdk:"cf_guid"` - CfSpaceGuid types.String `tfsdk:"cf_space_guid"` - DashboardUrl types.String `tfsdk:"dashboard_url"` - ImageUrl types.String `tfsdk:"image_url"` - Name types.String `tfsdk:"name"` - CfOrganizationGuid types.String `tfsdk:"cf_organization_guid"` - Parameters types.Object `tfsdk:"parameters"` - Version types.String `tfsdk:"version"` - PlanName types.String `tfsdk:"plan_name"` - PlanId types.String `tfsdk:"plan_id"` -} - -// Struct corresponding to DataSourceModel.Parameters -type parametersModel struct { - SgwAcl types.String `tfsdk:"sgw_acl"` - EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` - Graphite types.String `tfsdk:"graphite"` - MaxDiskThreshold types.Int64 `tfsdk:"max_disk_threshold"` - MetricsFrequency types.Int64 `tfsdk:"metrics_frequency"` - MetricsPrefix types.String `tfsdk:"metrics_prefix"` - MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"` - Syslog types.List `tfsdk:"syslog"` -} - -// Types corresponding to parametersModel -var parametersTypes = map[string]attr.Type{ - "sgw_acl": basetypes.StringType{}, - "enable_monitoring": basetypes.BoolType{}, - "graphite": basetypes.StringType{}, - "max_disk_threshold": basetypes.Int64Type{}, - "metrics_frequency": basetypes.Int64Type{}, - "metrics_prefix": basetypes.StringType{}, - "monitoring_instance_id": basetypes.StringType{}, - "syslog": basetypes.ListType{ElemType: types.StringType}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *mariadb.APIClient -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mariadb_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "MariaDB instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "MariaDB instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the MariaDB instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - "parameters": "Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.", - "enable_monitoring": "Enable monitoring.", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted.", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance. Monitoring instances with the plan \"Observability-Monitoring-Starter\" are not supported.", - "syslog": "List of syslog servers to send logs to.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Required: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Required: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Description: descriptions["parameters"], - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Optional: true, - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Optional: true, - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Optional: true, - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Optional: true, - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Optional: true, - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Optional: true, - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "syslog": schema.ListAttribute{ - Description: parametersDescriptions["syslog"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - }, - Optional: true, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "image_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.InstanceId - ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MariaDB instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MariaDB instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - err = r.client.PartialUpdateInstance(ctx, projectId, instanceId).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MariaDB instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "MariaDB instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "MariaDB instance state imported") -} - -func mapFields(instance *mariadb.Instance, model *Model) error { - if instance == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.InstanceId != nil { - instanceId = *instance.InstanceId - } else { - return fmt.Errorf("instance id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) - model.InstanceId = types.StringValue(instanceId) - model.PlanId = types.StringPointerValue(instance.PlanId) - model.CfGuid = types.StringPointerValue(instance.CfGuid) - model.CfSpaceGuid = types.StringPointerValue(instance.CfSpaceGuid) - model.DashboardUrl = types.StringPointerValue(instance.DashboardUrl) - model.ImageUrl = types.StringPointerValue(instance.ImageUrl) - model.Name = types.StringPointerValue(instance.Name) - model.CfOrganizationGuid = types.StringPointerValue(instance.CfOrganizationGuid) - - if instance.Parameters == nil { - model.Parameters = types.ObjectNull(parametersTypes) - } else { - parameters, err := mapParameters(*instance.Parameters) - if err != nil { - return fmt.Errorf("mapping parameters: %w", err) - } - model.Parameters = parameters - } - return nil -} - -func mapParameters(params map[string]interface{}) (types.Object, error) { - attributes := map[string]attr.Value{} - for attribute := range parametersTypes { - valueInterface, ok := params[attribute] - if !ok { - // All fields are optional, so this is ok - // Set the value as nil, will be handled accordingly - valueInterface = nil - } - - var value attr.Value - switch parametersTypes[attribute].(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) - case basetypes.StringType: - if valueInterface == nil { - value = types.StringNull() - } else { - valueString, ok := valueInterface.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) - } - value = types.StringValue(valueString) - } - case basetypes.BoolType: - if valueInterface == nil { - value = types.BoolNull() - } else { - valueBool, ok := valueInterface.(bool) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) - } - value = types.BoolValue(valueBool) - } - case basetypes.Int64Type: - if valueInterface == nil { - value = types.Int64Null() - } else { - // This may be int64, int32, int or float64 - // We try to assert all 4 - var valueInt64 int64 - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) - case int64: - valueInt64 = temp - case int32: - valueInt64 = int64(temp) - case int: - valueInt64 = int64(temp) - case float64: - valueInt64 = int64(temp) - } - value = types.Int64Value(valueInt64) - } - case basetypes.ListType: // Assumed to be a list of strings - if valueInterface == nil { - value = types.ListNull(types.StringType) - } else { - // This may be []string{} or []interface{} - // We try to assert all 2 - var valueList []attr.Value - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) - case []string: - for _, x := range temp { - valueList = append(valueList, types.StringValue(x)) - } - case []interface{}: - for _, x := range temp { - xString, ok := x.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) - } - valueList = append(valueList, types.StringValue(xString)) - } - } - temp2, diags := types.ListValue(types.StringType, valueList) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) - } - value = temp2 - } - } - attributes[attribute] = value - } - - output, diags := types.ObjectValue(parametersTypes, attributes) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) - } - return output, nil -} - -func toCreatePayload(model *Model, parameters *parametersModel) (*mariadb.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("convert parameters: %w", err) - } - return &mariadb.CreateInstancePayload{ - InstanceName: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - Parameters: payloadParams, - }, nil -} - -func toUpdatePayload(model *Model, parameters *parametersModel) (*mariadb.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("convert parameters: %w", err) - } - return &mariadb.PartialUpdateInstancePayload{ - PlanId: conversion.StringValueToPointer(model.PlanId), - Parameters: payloadParams, - }, nil -} - -func toInstanceParams(parameters *parametersModel) (*mariadb.InstanceParameters, error) { - if parameters == nil { - return nil, nil - } - payloadParams := &mariadb.InstanceParameters{} - - payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) - payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring) - payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite) - payloadParams.MaxDiskThreshold = conversion.Int64ValueToPointer(parameters.MaxDiskThreshold) - payloadParams.MetricsFrequency = conversion.Int64ValueToPointer(parameters.MetricsFrequency) - payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix) - payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) - - syslog, err := conversion.StringListToPointer(parameters.Syslog) - if err != nil { - return nil, fmt.Errorf("convert syslog: %w", err) - } - payloadParams.Syslog = syslog - - return payloadParams, nil -} - -func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { - projectId := model.ProjectId.ValueString() - res, err := r.client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting MariaDB offerings: %w", err) - } - - version := model.Version.ValueString() - planName := model.PlanName.ValueString() - availableVersions := "" - availablePlanNames := "" - isValidVersion := false - for _, offer := range *res.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) - continue - } - isValidVersion = true - - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue - } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - model.PlanId = types.StringPointerValue(plan.Id) - return nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) - } - } - - if !isValidVersion { - return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) - } - return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) -} - -func loadPlanNameAndVersion(ctx context.Context, client *mariadb.APIClient, model *Model) error { - projectId := model.ProjectId.ValueString() - planId := model.PlanId.ValueString() - res, err := client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting MariaDB offerings: %w", err) - } - - for _, offer := range *res.Offerings { - for _, plan := range *offer.Plans { - if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { - model.PlanName = types.StringPointerValue(plan.Name) - model.Version = types.StringPointerValue(offer.Version) - return nil - } - } - } - - return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) -} diff --git a/stackit/internal/services/mariadb/instance/resource_test.go b/stackit/internal/services/mariadb/instance/resource_test.go deleted file mode 100644 index c9a1af9d..00000000 --- a/stackit/internal/services/mariadb/instance/resource_test.go +++ /dev/null @@ -1,339 +0,0 @@ -package mariadb - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" -) - -var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringValue("acl"), - "enable_monitoring": types.BoolValue(true), - "graphite": types.StringValue("graphite"), - "max_disk_threshold": types.Int64Value(10), - "metrics_frequency": types.Int64Value(10), - "metrics_prefix": types.StringValue("prefix"), - "monitoring_instance_id": types.StringValue("mid"), - "syslog": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("syslog"), - types.StringValue("syslog2"), - }), -}) - -var fixtureNullModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringNull(), - "enable_monitoring": types.BoolNull(), - "graphite": types.StringNull(), - "max_disk_threshold": types.Int64Null(), - "metrics_frequency": types.Int64Null(), - "metrics_prefix": types.StringNull(), - "monitoring_instance_id": types.StringNull(), - "syslog": types.ListNull(types.StringType), -}) - -var fixtureInstanceParameters = mariadb.InstanceParameters{ - SgwAcl: utils.Ptr("acl"), - EnableMonitoring: utils.Ptr(true), - Graphite: utils.Ptr("graphite"), - MaxDiskThreshold: utils.Ptr(int64(10)), - MetricsFrequency: utils.Ptr(int64(10)), - MetricsPrefix: utils.Ptr("prefix"), - MonitoringInstanceId: utils.Ptr("mid"), - Syslog: &[]string{"syslog", "syslog2"}, -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *mariadb.Instance - expected Model - isValid bool - }{ - { - "default_values", - &mariadb.Instance{}, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringNull(), - Name: types.StringNull(), - CfGuid: types.StringNull(), - CfSpaceGuid: types.StringNull(), - DashboardUrl: types.StringNull(), - ImageUrl: types.StringNull(), - CfOrganizationGuid: types.StringNull(), - Parameters: types.ObjectNull(parametersTypes), - }, - true, - }, - { - "simple_values", - &mariadb.Instance{ - PlanId: utils.Ptr("plan"), - CfGuid: utils.Ptr("cf"), - CfSpaceGuid: utils.Ptr("space"), - DashboardUrl: utils.Ptr("dashboard"), - ImageUrl: utils.Ptr("image"), - InstanceId: utils.Ptr("iid"), - Name: utils.Ptr("name"), - CfOrganizationGuid: utils.Ptr("org"), - Parameters: &map[string]interface{}{ - "sgw_acl": "acl", - "enable_monitoring": true, - "graphite": "graphite", - "max_disk_threshold": int64(10), - "metrics_frequency": int64(10), - "metrics_prefix": "prefix", - "monitoring_instance_id": "mid", - "syslog": []string{"syslog", "syslog2"}, - }, - }, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringValue("plan"), - Name: types.StringValue("name"), - CfGuid: types.StringValue("cf"), - CfSpaceGuid: types.StringValue("space"), - DashboardUrl: types.StringValue("dashboard"), - ImageUrl: types.StringValue("image"), - CfOrganizationGuid: types.StringValue("org"), - Parameters: fixtureModelParameters, - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &mariadb.Instance{}, - Model{}, - false, - }, - { - "wrong_param_types_1", - &mariadb.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": true, - }, - }, - Model{}, - false, - }, - { - "wrong_param_types_2", - &mariadb.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": 1, - }, - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *mariadb.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &mariadb.CreateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &mariadb.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - PlanId: types.StringValue(""), - Parameters: fixtureNullModelParameters, - }, - &mariadb.CreateInstancePayload{ - InstanceName: utils.Ptr(""), - Parameters: &mariadb.InstanceParameters{}, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - }, - &mariadb.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toCreatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *mariadb.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &mariadb.PartialUpdateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &mariadb.PartialUpdateInstancePayload{ - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - PlanId: types.StringValue(""), - Parameters: fixtureNullModelParameters, - }, - &mariadb.PartialUpdateInstancePayload{ - Parameters: &mariadb.InstanceParameters{}, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - PlanId: types.StringValue("plan"), - }, - &mariadb.PartialUpdateInstancePayload{ - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toUpdatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/mariadb/mariadb_acc_test.go b/stackit/internal/services/mariadb/mariadb_acc_test.go deleted file mode 100644 index 3d40b877..00000000 --- a/stackit/internal/services/mariadb/mariadb_acc_test.go +++ /dev/null @@ -1,476 +0,0 @@ -package mariadb_test - -import ( - "context" - _ "embed" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -//go:embed testfiles/resource-min.tf -var resourceMinConfig string - -//go:embed testfiles/resource-max.tf -var resourceMaxConfig string - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "plan_name": config.StringVariable("stackit-mariadb-1.4.10-single"), - "db_version": config.StringVariable("10.6"), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "plan_name": config.StringVariable("stackit-mariadb-1.4.10-single"), - "db_version": config.StringVariable("10.11"), - "observability_instance_plan_name": config.StringVariable("Observability-Monitoring-Basic-EU01"), - "parameters_enable_monitoring": config.BoolVariable(true), - "parameters_graphite": config.StringVariable(fmt.Sprintf("%s.graphite.stackit.cloud:2003", acctest.RandStringFromCharSet(7, acctest.CharSetAlpha))), - "parameters_max_disk_threshold": config.IntegerVariable(75), - "parameters_metrics_frequency": config.IntegerVariable(15), - "parameters_metrics_prefix": config.StringVariable("acc-test"), - "parameters_sgw_acl": config.StringVariable("192.168.2.0/24"), - "parameters_syslog": config.StringVariable("acc.test.log:514"), -} - -func configVarsMaxUpdated() config.Variables { - updatedConfig := config.Variables{} - for k, v := range testConfigVarsMax { - updatedConfig[k] = v - } - updatedConfig["parameters_max_disk_threshold"] = config.IntegerVariable(85) - updatedConfig["parameters_metrics_frequency"] = config.IntegerVariable(10) - updatedConfig["parameters_graphite"] = config.StringVariable("graphite.stackit.cloud:2003") - updatedConfig["parameters_sgw_acl"] = config.StringVariable("192.168.1.0/24") - updatedConfig["parameters_syslog"] = config.StringVariable("test.log:514") - return updatedConfig -} - -// minimum configuration -func TestAccMariaDbResourceMin(t *testing.T) { - t.Logf("Maria test instance name: %s", testutil.ConvertConfigVariable(testConfigVarsMin["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckMariaDBDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMin, - Config: fmt.Sprintf("%s\n%s", testutil.MariaDBProviderConfig(), resourceMinConfig), - Check: resource.ComposeTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMin["db_version"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMin["plan_name"])), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "instance_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "plan_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_organization_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_space_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "image_url"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "dashboard_url"), - - // Credential - resource.TestCheckResourceAttrPair( - "stackit_mariadb_instance.instance", "project_id", - "stackit_mariadb_credential.credential", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_mariadb_instance.instance", "instance_id", - "stackit_mariadb_credential.credential", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "host"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "hosts.#"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "name"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "password"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "port"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "username"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMin, - Config: fmt.Sprintf(` - %s - - %s - - data "stackit_mariadb_instance" "instance" { - project_id = stackit_mariadb_instance.instance.project_id - instance_id = stackit_mariadb_instance.instance.instance_id - } - - data "stackit_mariadb_credential" "credential" { - project_id = stackit_mariadb_credential.credential.project_id - instance_id = stackit_mariadb_credential.credential.instance_id - credential_id = stackit_mariadb_credential.credential.credential_id - }`, testutil.MariaDBProviderConfig(), resourceMinConfig, - ), - Check: resource.ComposeTestCheckFunc( - // Instance data - resource.TestCheckResourceAttrPair( - "data.stackit_mariadb_instance.instance", "instance_id", - "stackit_mariadb_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMin["db_version"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMin["plan_name"])), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "plan_id"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "cf_guid"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "cf_organization_guid"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "cf_space_guid"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "image_url"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "dashboard_url"), - - // Credential data - resource.TestCheckResourceAttr("data.stackit_mariadb_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "host"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "hosts.#"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "name"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "password"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "port"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "username"), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_mariadb_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_mariadb_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_mariadb_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_mariadb_credential.credential", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_mariadb_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_mariadb_credential.credential") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // In this minimal setup, it's not possible to perform an update - // Deletion is done by the framework implicitly - }, - }) -} - -// maximum configuration -func TestAccMariaDbResourceMax(t *testing.T) { - t.Logf("Maria test instance name: %s", testutil.ConvertConfigVariable(testConfigVarsMax["name"])) - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckMariaDBDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMax, - Config: fmt.Sprintf("%s\n%s", testutil.MariaDBProviderConfig(), resourceMaxConfig), - Check: resource.ComposeTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMax["db_version"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMax["plan_name"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.enable_monitoring", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_enable_monitoring"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.graphite", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_graphite"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.max_disk_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_max_disk_threshold"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.metrics_frequency", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_metrics_frequency"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.metrics_prefix", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_metrics_prefix"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.sgw_acl", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_sgw_acl"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_syslog"])), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "parameters.monitoring_instance_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "instance_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "plan_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_organization_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_space_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "image_url"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "dashboard_url"), - - // Credential - resource.TestCheckResourceAttrPair( - "stackit_mariadb_instance.instance", "project_id", - "stackit_mariadb_credential.credential", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_mariadb_instance.instance", "instance_id", - "stackit_mariadb_credential.credential", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "host"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "hosts.#"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "name"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "password"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "port"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "username"), - - // Observability - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.observability_instance", "instance_id", - "stackit_mariadb_instance.instance", "parameters.monitoring_instance_id", - ), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMax, - Config: fmt.Sprintf(` - %s - - %s - - data "stackit_mariadb_instance" "instance" { - project_id = stackit_mariadb_instance.instance.project_id - instance_id = stackit_mariadb_instance.instance.instance_id - } - - data "stackit_mariadb_credential" "credential" { - project_id = stackit_mariadb_credential.credential.project_id - instance_id = stackit_mariadb_credential.credential.instance_id - credential_id = stackit_mariadb_credential.credential.credential_id - }`, testutil.MariaDBProviderConfig(), resourceMaxConfig, - ), - Check: resource.ComposeTestCheckFunc( - // Instance data - resource.TestCheckResourceAttrPair( - "data.stackit_mariadb_instance.instance", "instance_id", - "stackit_mariadb_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_mariadb_instance.instance", "project_id", - "stackit_mariadb_instance.instance", "project_id", - ), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMax["db_version"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMax["plan_name"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "parameters.enable_monitoring", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_enable_monitoring"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "parameters.graphite", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_graphite"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "parameters.max_disk_threshold", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_max_disk_threshold"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "parameters.metrics_frequency", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_metrics_frequency"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "parameters.metrics_prefix", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_metrics_prefix"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "parameters.sgw_acl", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_sgw_acl"])), - resource.TestCheckResourceAttr("data.stackit_mariadb_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(testConfigVarsMax["parameters_syslog"])), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "plan_id"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "cf_guid"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "cf_organization_guid"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "cf_space_guid"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "image_url"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_instance.instance", "dashboard_url"), - - // Credential data - resource.TestCheckResourceAttr("data.stackit_mariadb_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "host"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "hosts.#"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "name"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "password"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "port"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("data.stackit_mariadb_credential.credential", "username"), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_mariadb_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_mariadb_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_mariadb_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_mariadb_credential.credential", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_mariadb_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_mariadb_credential.credential") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: configVarsMaxUpdated(), - Config: fmt.Sprintf("%s\n%s", testutil.MariaDBProviderConfig(), resourceMaxConfig), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "version", testutil.ConvertConfigVariable(configVarsMaxUpdated()["db_version"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "plan_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["plan_name"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.enable_monitoring", testutil.ConvertConfigVariable(configVarsMaxUpdated()["parameters_enable_monitoring"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.graphite", testutil.ConvertConfigVariable(configVarsMaxUpdated()["parameters_graphite"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.max_disk_threshold", testutil.ConvertConfigVariable(configVarsMaxUpdated()["parameters_max_disk_threshold"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.metrics_frequency", testutil.ConvertConfigVariable(configVarsMaxUpdated()["parameters_metrics_frequency"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.metrics_prefix", testutil.ConvertConfigVariable(configVarsMaxUpdated()["parameters_metrics_prefix"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.sgw_acl", testutil.ConvertConfigVariable(configVarsMaxUpdated()["parameters_sgw_acl"])), - resource.TestCheckResourceAttr("stackit_mariadb_instance.instance", "parameters.syslog.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["parameters_syslog"])), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "instance_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "plan_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_organization_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "cf_space_guid"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "image_url"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("stackit_mariadb_instance.instance", "parameters.monitoring_instance_id"), - - // Credential - resource.TestCheckResourceAttrPair( - "stackit_mariadb_instance.instance", "project_id", - "stackit_mariadb_credential.credential", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_mariadb_instance.instance", "instance_id", - "stackit_mariadb_credential.credential", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "host"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "hosts.#"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "name"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "password"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "port"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("stackit_mariadb_credential.credential", "username"), - - // Observability - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.observability_instance", "instance_id", - "stackit_mariadb_instance.instance", "parameters.monitoring_instance_id", - ), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckMariaDBDestroy(s *terraform.State) error { - ctx := context.Background() - var client *mariadb.APIClient - var err error - if testutil.MariaDBCustomEndpoint == "" { - client, err = mariadb.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) - } else { - client, err = mariadb.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.MariaDBCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_mariadb_instance" { - continue - } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := *instancesResp.Instances - for i := range instances { - if instances[i].InstanceId == nil { - continue - } - if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { - if !checkInstanceDeleteSuccess(&instances[i]) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) - } - } - } - } - return nil -} - -func checkInstanceDeleteSuccess(i *mariadb.Instance) bool { - if *i.LastOperation.Type != mariadb.INSTANCELASTOPERATIONTYPE_DELETE { - return false - } - - if *i.LastOperation.Type == mariadb.INSTANCELASTOPERATIONTYPE_DELETE { - if *i.LastOperation.State != mariadb.INSTANCELASTOPERATIONSTATE_SUCCEEDED { - return false - } else if strings.Contains(*i.LastOperation.Description, "DeleteFailed") || strings.Contains(*i.LastOperation.Description, "failed") { - return false - } - } - return true -} diff --git a/stackit/internal/services/mariadb/testfiles/resource-max.tf b/stackit/internal/services/mariadb/testfiles/resource-max.tf deleted file mode 100644 index 2198c8a5..00000000 --- a/stackit/internal/services/mariadb/testfiles/resource-max.tf +++ /dev/null @@ -1,40 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "db_version" {} -variable "plan_name" {} -variable "observability_instance_plan_name" {} -variable "parameters_enable_monitoring" {} -variable "parameters_graphite" {} -variable "parameters_max_disk_threshold" {} -variable "parameters_metrics_frequency" {} -variable "parameters_metrics_prefix" {} -variable "parameters_sgw_acl" {} -variable "parameters_syslog" {} - -resource "stackit_observability_instance" "observability_instance" { - project_id = var.project_id - name = var.name - plan_name = var.observability_instance_plan_name -} - -resource "stackit_mariadb_instance" "instance" { - project_id = var.project_id - name = var.name - version = var.db_version - plan_name = var.plan_name - parameters = { - enable_monitoring = var.parameters_enable_monitoring - graphite = var.parameters_graphite - max_disk_threshold = var.parameters_max_disk_threshold - metrics_frequency = var.parameters_metrics_frequency - metrics_prefix = var.parameters_metrics_prefix - monitoring_instance_id = stackit_observability_instance.observability_instance.instance_id - sgw_acl = var.parameters_sgw_acl - syslog = [var.parameters_syslog] - } -} - -resource "stackit_mariadb_credential" "credential" { - project_id = var.project_id - instance_id = stackit_mariadb_instance.instance.instance_id -} \ No newline at end of file diff --git a/stackit/internal/services/mariadb/testfiles/resource-min.tf b/stackit/internal/services/mariadb/testfiles/resource-min.tf deleted file mode 100644 index f8a55744..00000000 --- a/stackit/internal/services/mariadb/testfiles/resource-min.tf +++ /dev/null @@ -1,16 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "db_version" {} -variable "plan_name" {} - -resource "stackit_mariadb_instance" "instance" { - project_id = var.project_id - name = var.name - version = var.db_version - plan_name = var.plan_name -} - -resource "stackit_mariadb_credential" "credential" { - project_id = var.project_id - instance_id = stackit_mariadb_instance.instance.instance_id -} \ No newline at end of file diff --git a/stackit/internal/services/mariadb/utils/util.go b/stackit/internal/services/mariadb/utils/util.go deleted file mode 100644 index 21928e16..00000000 --- a/stackit/internal/services/mariadb/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *mariadb.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.MariaDBCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.MariaDBCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := mariadb.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/mariadb/utils/util_test.go b/stackit/internal/services/mariadb/utils/util_test.go deleted file mode 100644 index 88dfa102..00000000 --- a/stackit/internal/services/mariadb/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://mariadb-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *mariadb.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *mariadb.APIClient { - apiClient, err := mariadb.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - MariaDBCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *mariadb.APIClient { - apiClient, err := mariadb.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/modelserving/modelserving_acc_test.go b/stackit/internal/services/modelserving/modelserving_acc_test.go deleted file mode 100644 index 4f31d5ca..00000000 --- a/stackit/internal/services/modelserving/modelserving_acc_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package modelserving_test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/modelserving" - "github.com/stackitcloud/stackit-sdk-go/services/modelserving/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// Token resource data -var tokenResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": "token01", - "description": "my description", - "description_updated": "my description updated", - "region": testutil.Region, - "ttl_duration": "1h", -} - -func inputTokenConfig(name, description string) string { - return fmt.Sprintf(` - %s - - resource "stackit_modelserving_token" "token" { - project_id = "%s" - region = "%s" - name = "%s" - description = "%s" - ttl_duration = "%s" - } - `, - testutil.ModelServingProviderConfig(), - tokenResource["project_id"], - tokenResource["region"], - name, - description, - tokenResource["ttl_duration"], - ) -} - -func TestAccModelServingTokenResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckModelServingTokenDestroy, - Steps: []resource.TestStep{ - // Creation - { - Config: inputTokenConfig( - tokenResource["name"], - tokenResource["description"], - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "project_id", tokenResource["project_id"]), - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "region", tokenResource["region"]), - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "name", tokenResource["name"]), - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "description", tokenResource["description"]), - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "ttl_duration", tokenResource["ttl_duration"]), - resource.TestCheckResourceAttrSet("stackit_modelserving_token.token", "token_id"), - resource.TestCheckResourceAttrSet("stackit_modelserving_token.token", "state"), - resource.TestCheckResourceAttrSet("stackit_modelserving_token.token", "valid_until"), - resource.TestCheckResourceAttrSet("stackit_modelserving_token.token", "token"), - ), - }, - // Update - { - Config: inputTokenConfig( - tokenResource["name"], - tokenResource["description_updated"], - ), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "project_id", tokenResource["project_id"]), - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "region", tokenResource["region"]), - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "name", tokenResource["name"]), - resource.TestCheckResourceAttr("stackit_modelserving_token.token", "description", tokenResource["description_updated"]), - resource.TestCheckResourceAttrSet("stackit_modelserving_token.token", "token_id"), - resource.TestCheckResourceAttrSet("stackit_modelserving_token.token", "state"), - resource.TestCheckResourceAttrSet("stackit_modelserving_token.token", "valid_until"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckModelServingTokenDestroy(s *terraform.State) error { - ctx := context.Background() - var client *modelserving.APIClient - var err error - - if testutil.ModelServingCustomEndpoint == "" { - client, err = modelserving.NewAPIClient() - } else { - client, err = modelserving.NewAPIClient( - config.WithEndpoint(testutil.ModelServingCustomEndpoint), - ) - } - - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - tokensToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_modelserving_token" { - continue - } - - // Token terraform ID: "[project_id],[region],[token_id]" - idParts := strings.Split(rs.Primary.ID, core.Separator) - if len(idParts) != 3 { - return fmt.Errorf("invalid ID: %s", rs.Primary.ID) - } - if idParts[2] != "" { - tokensToDestroy = append(tokensToDestroy, idParts[2]) - } - } - - if len(tokensToDestroy) == 0 { - return nil - } - - tokensResp, err := client.ListTokens(ctx, testutil.Region, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting tokensResp: %w", err) - } - - if tokensResp.Tokens == nil || (tokensResp.Tokens != nil && len(*tokensResp.Tokens) == 0) { - fmt.Print("No tokens found for project \n") - return nil - } - - items := *tokensResp.Tokens - for i := range items { - if items[i].Name == nil { - continue - } - if utils.Contains(tokensToDestroy, *items[i].Name) { - _, err := client.DeleteToken(ctx, testutil.Region, testutil.ProjectId, *items[i].Id).Execute() - if err != nil { - return fmt.Errorf("destroying token %s during CheckDestroy: %w", *items[i].Name, err) - } - _, err = wait.DeleteModelServingWaitHandler(ctx, client, testutil.Region, testutil.ProjectId, *items[i].Id).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying token %s during CheckDestroy: waiting for deletion %w", *items[i].Name, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/modelserving/token/description.md b/stackit/internal/services/modelserving/token/description.md deleted file mode 100644 index 236385a1..00000000 --- a/stackit/internal/services/modelserving/token/description.md +++ /dev/null @@ -1,20 +0,0 @@ -AI Model Serving Auth Token Resource schema. - -## Example Usage - -### Automatically rotate AI model serving token -```terraform -resource "time_rotating" "rotate" { - rotation_days = 80 -} - -resource "stackit_modelserving_token" "example" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "Example token" - - rotate_when_changed = { - rotation = time_rotating.rotate.id - } - -} -``` \ No newline at end of file diff --git a/stackit/internal/services/modelserving/token/resource.go b/stackit/internal/services/modelserving/token/resource.go deleted file mode 100644 index c55ba150..00000000 --- a/stackit/internal/services/modelserving/token/resource.go +++ /dev/null @@ -1,615 +0,0 @@ -package token - -import ( - "context" - _ "embed" - "errors" - "fmt" - "net/http" - "time" - - modelservingUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/modelserving/utils" - serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/modelserving" - "github.com/stackitcloud/stackit-sdk-go/services/modelserving/wait" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" - serviceEnablementWait "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &tokenResource{} - _ resource.ResourceWithConfigure = &tokenResource{} - _ resource.ResourceWithModifyPlan = &tokenResource{} -) - -const ( - inactiveState = "inactive" -) - -//go:embed description.md -var markdownDescription string - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Region types.String `tfsdk:"region"` - TokenId types.String `tfsdk:"token_id"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - State types.String `tfsdk:"state"` - ValidUntil types.String `tfsdk:"valid_until"` - TTLDuration types.String `tfsdk:"ttl_duration"` - Token types.String `tfsdk:"token"` - // RotateWhenChanged is a map of arbitrary key/value pairs that will force - // recreation of the token when they change, enabling token rotation based on - // external conditions such as a rotating timestamp. Changing this forces a new - // resource to be created. - RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"` -} - -// NewTokenResource is a helper function to simplify the provider implementation. -func NewTokenResource() resource.Resource { - return &tokenResource{} -} - -// tokenResource is the resource implementation. -type tokenResource struct { - client *modelserving.APIClient - providerData core.ProviderData - serviceEnablementClient *serviceenablement.APIClient -} - -// Metadata returns the resource type name. -func (r *tokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_modelserving_token" -} - -// Configure adds the provider configured client to the resource. -func (r *tokenResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := modelservingUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - serviceEnablementClient := serviceenablementUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - r.serviceEnablementClient = serviceEnablementClient - tflog.Info(ctx, "Model-Serving auth token client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *tokenResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion( - ctx, - configModel.Region, - &planModel.Region, - r.providerData.GetRegion(), - resp, - ) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Schema defines the schema for the resource. -func (r *tokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: markdownDescription, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`token_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the AI model serving auth token is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "Region to which the AI model serving auth token is associated. If not defined, the provider region is used", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "token_id": schema.StringAttribute{ - Description: "The AI model serving auth token ID.", - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "ttl_duration": schema.StringAttribute{ - Description: "The TTL duration of the AI model serving auth token. E.g. 5h30m40s,5h,5h30m,30m,30s", - Required: false, - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.ValidDurationString(), - }, - }, - "rotate_when_changed": schema.MapAttribute{ - Description: "A map of arbitrary key/value pairs that will force " + - "recreation of the token when they change, enabling token rotation " + - "based on external conditions such as a rotating timestamp. Changing " + - "this forces a new resource to be created.", - Optional: true, - Required: false, - ElementType: types.StringType, - PlanModifiers: []planmodifier.Map{ - mapplanmodifier.RequiresReplace(), - }, - }, - "description": schema.StringAttribute{ - Description: "The description of the AI model serving auth token.", - Required: false, - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 2000), - }, - }, - "name": schema.StringAttribute{ - Description: "Name of the AI model serving auth token.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 200), - }, - }, - "state": schema.StringAttribute{ - Description: "State of the AI model serving auth token.", - Computed: true, - }, - "token": schema.StringAttribute{ - Description: "Content of the AI model serving auth token.", - Computed: true, - Sensitive: true, - }, - "valid_until": schema.StringAttribute{ - Description: "The time until the AI model serving auth token is valid.", - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - // If AI model serving is not enabled, enable it - err := r.serviceEnablementClient.EnableServiceRegional(ctx, region, projectId, utils.ModelServingServiceId). - Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error enabling AI model serving", - fmt.Sprintf("Service not available in region %s \n%v", region, err), - ) - return - } - } - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error enabling AI model serving", - fmt.Sprintf("Error enabling AI model serving: %v", err), - ) - return - } - - _, err = serviceEnablementWait.EnableServiceWaitHandler(ctx, r.serviceEnablementClient, region, projectId, utils.ModelServingServiceId). - WaitWithContext(ctx) - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error enabling AI model serving", - fmt.Sprintf("Error enabling AI model serving: %v", err), - ) - return - } - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating AI model serving auth token", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create new AI model serving auth token - createTokenResp, err := r.client.CreateToken(ctx, region, projectId). - CreateTokenPayload(*payload). - Execute() - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error creating AI model serving auth token", - fmt.Sprintf("Calling API: %v", err), - ) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.CreateModelServingWaitHandler(ctx, r.client, region, projectId, *createTokenResp.Token.Id).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating AI model serving auth token", fmt.Sprintf("Waiting for token to be active: %v", err)) - return - } - - // Map response body to schema - err = mapCreateResponse(createTokenResp, waitResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating AI model serving auth token", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Model-Serving auth token created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *tokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - tokenId := model.TokenId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "token_id", tokenId) - ctx = tflog.SetField(ctx, "region", region) - - getTokenResp, err := r.client.GetToken(ctx, region, projectId, tokenId). - Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - // Remove the resource from the state so Terraform will recreate it - resp.State.RemoveResource(ctx) - return - } - } - - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading AI model serving auth token", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if getTokenResp != nil && getTokenResp.Token.State != nil && - *getTokenResp.Token.State == inactiveState { - resp.State.RemoveResource(ctx) - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Error reading AI model serving auth token", "AI model serving auth token has expired") - return - } - - // Map response body to schema - err = mapGetResponse(getTokenResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading AI model serving auth token", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Model-Serving auth token read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Get current state - var state Model - diags = req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := state.ProjectId.ValueString() - tokenId := state.TokenId.ValueString() - - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "token_id", tokenId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating AI model serving auth token", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Update AI model serving auth token - updateTokenResp, err := r.client.PartialUpdateToken(ctx, region, projectId, tokenId).PartialUpdateTokenPayload(*payload).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - // Remove the resource from the state so Terraform will recreate it - resp.State.RemoveResource(ctx) - return - } - } - - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error updating AI model serving auth token", - fmt.Sprintf( - "Calling API: %v, tokenId: %s, region: %s, projectId: %s", - err, - tokenId, - region, - projectId, - ), - ) - return - } - - ctx = core.LogResponse(ctx) - - if updateTokenResp != nil && updateTokenResp.Token.State != nil && - *updateTokenResp.Token.State == inactiveState { - resp.State.RemoveResource(ctx) - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Error updating AI model serving auth token", "AI model serving auth token has expired") - return - } - - waitResp, err := wait.UpdateModelServingWaitHandler(ctx, r.client, region, projectId, tokenId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating AI model serving auth token", fmt.Sprintf("Waiting for token to be updated: %v", err)) - return - } - - // Since STACKIT is not saving the content of the token. We have to use it from the state. - model.Token = state.Token - err = mapGetResponse(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating AI model serving auth token", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "Model-Serving auth token updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *tokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - tokenId := model.TokenId.ValueString() - - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "token_id", tokenId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing AI model serving auth token. We will ignore the state 'deleting' for now. - _, err := r.client.DeleteToken(ctx, region, projectId, tokenId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - } - - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting AI model serving auth token", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteModelServingWaitHandler(ctx, r.client, region, projectId, tokenId). - WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting AI model serving auth token", fmt.Sprintf("Waiting for token to be deleted: %v", err)) - return - } - - tflog.Info(ctx, "Model-Serving auth token deleted") -} - -func mapCreateResponse(tokenCreateResp *modelserving.CreateTokenResponse, waitResp *modelserving.GetTokenResponse, model *Model, region string) error { - if tokenCreateResp == nil || tokenCreateResp.Token == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - token := tokenCreateResp.Token - - if token.Id == nil { - return fmt.Errorf("token id not present") - } - - validUntil := types.StringNull() - if token.ValidUntil != nil { - validUntil = types.StringValue(token.ValidUntil.Format(time.RFC3339)) - } - - if waitResp == nil || waitResp.Token == nil || waitResp.Token.State == nil { - return fmt.Errorf("response input is nil") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, *tokenCreateResp.Token.Id) - model.TokenId = types.StringPointerValue(token.Id) - model.Name = types.StringPointerValue(token.Name) - model.State = types.StringValue(string(waitResp.Token.GetState())) - model.ValidUntil = validUntil - model.Token = types.StringPointerValue(token.Content) - model.Description = types.StringPointerValue(token.Description) - - return nil -} - -func mapGetResponse(tokenGetResp *modelserving.GetTokenResponse, model *Model) error { - if tokenGetResp == nil { - return fmt.Errorf("response input is nil") - } - - if tokenGetResp.Token == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - // theoretically, should never happen, but still catch null pointers - validUntil := types.StringNull() - if tokenGetResp.Token.ValidUntil != nil { - validUntil = types.StringValue(tokenGetResp.Token.ValidUntil.Format(time.RFC3339)) - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.Region.ValueString(), model.TokenId.ValueString()) - model.TokenId = types.StringPointerValue(tokenGetResp.Token.Id) - model.Name = types.StringPointerValue(tokenGetResp.Token.Name) - model.State = types.StringValue(string(tokenGetResp.Token.GetState())) - model.ValidUntil = validUntil - model.Description = types.StringPointerValue(tokenGetResp.Token.Description) - - return nil -} - -func toCreatePayload(model *Model) (*modelserving.CreateTokenPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &modelserving.CreateTokenPayload{ - Name: conversion.StringValueToPointer(model.Name), - Description: conversion.StringValueToPointer(model.Description), - TtlDuration: conversion.StringValueToPointer(model.TTLDuration), - }, nil -} - -func toUpdatePayload(model *Model) (*modelserving.PartialUpdateTokenPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &modelserving.PartialUpdateTokenPayload{ - Name: conversion.StringValueToPointer(model.Name), - Description: conversion.StringValueToPointer(model.Description), - }, nil -} diff --git a/stackit/internal/services/modelserving/token/resource_test.go b/stackit/internal/services/modelserving/token/resource_test.go deleted file mode 100644 index ddd305ff..00000000 --- a/stackit/internal/services/modelserving/token/resource_test.go +++ /dev/null @@ -1,341 +0,0 @@ -package token - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/modelserving" -) - -func TestMapGetTokenFields(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - state *Model - input *modelserving.GetTokenResponse - expected Model - isValid bool - }{ - { - description: "should error when response is nil", - state: &Model{}, - input: nil, - expected: Model{}, - isValid: false, - }, - { - description: "should error when token is nil in response", - state: &Model{}, - input: &modelserving.GetTokenResponse{Token: nil}, - expected: Model{}, - isValid: false, - }, - { - description: "should error when state is nil in response", - state: nil, - input: &modelserving.GetTokenResponse{ - Token: &modelserving.Token{}, - }, - expected: Model{}, - isValid: false, - }, - { - description: "should map fields correctly", - state: &Model{ - Id: types.StringValue("pid,eu01,tid"), - ProjectId: types.StringValue("pid"), - TokenId: types.StringValue("tid"), - Region: types.StringValue("eu01"), - RotateWhenChanged: types.MapNull(types.StringType), - }, - input: &modelserving.GetTokenResponse{ - Token: &modelserving.Token{ - Id: utils.Ptr("tid"), - ValidUntil: utils.Ptr( - time.Date(2099, 1, 1, 0, 0, 0, 0, time.UTC), - ), - State: modelserving.TOKENSTATE_ACTIVE.Ptr(), - Name: utils.Ptr("name"), - Description: utils.Ptr("desc"), - Region: utils.Ptr("eu01"), - }, - }, - expected: Model{ - Id: types.StringValue("pid,eu01,tid"), - ProjectId: types.StringValue("pid"), - Region: types.StringValue("eu01"), - TokenId: types.StringValue("tid"), - Name: types.StringValue("name"), - Description: types.StringValue("desc"), - State: types.StringValue(string(modelserving.TOKENSTATE_ACTIVE)), - ValidUntil: types.StringValue("2099-01-01T00:00:00Z"), - RotateWhenChanged: types.MapNull(types.StringType), - }, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - t.Parallel() - - err := mapGetResponse(tt.input, tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - - if tt.isValid { - diff := cmp.Diff(tt.state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapCreateTokenFields(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - state *Model - inputCreateTokenResponse *modelserving.CreateTokenResponse - inputGetTokenResponse *modelserving.GetTokenResponse - expected Model - isValid bool - }{ - { - description: "should error when create token response is nil", - state: &Model{}, - inputCreateTokenResponse: nil, - inputGetTokenResponse: nil, - expected: Model{}, - isValid: false, - }, - { - description: "should error when token is nil in create token response", - state: &Model{}, - inputCreateTokenResponse: &modelserving.CreateTokenResponse{ - Token: nil, - }, - inputGetTokenResponse: nil, - expected: Model{}, - isValid: false, - }, - { - description: "should error when get token response is nil", - state: &Model{}, - inputCreateTokenResponse: &modelserving.CreateTokenResponse{ - Token: &modelserving.TokenCreated{}, - }, - inputGetTokenResponse: nil, - expected: Model{}, - isValid: false, - }, - { - description: "should error when get token response is nil", - state: &Model{ - Id: types.StringValue("pid,eu01,tid"), - ProjectId: types.StringValue("pid"), - }, - inputCreateTokenResponse: &modelserving.CreateTokenResponse{ - Token: &modelserving.TokenCreated{ - Id: utils.Ptr("tid"), - ValidUntil: utils.Ptr( - time.Date(2099, 1, 1, 0, 0, 0, 0, time.UTC), - ), - State: modelserving.TOKENCREATEDSTATE_ACTIVE.Ptr(), - Name: utils.Ptr("name"), - Description: utils.Ptr("desc"), - Region: utils.Ptr("eu01"), - Content: utils.Ptr("content"), - }, - }, - inputGetTokenResponse: nil, - expected: Model{}, - isValid: false, - }, - { - description: "should map fields correctly", - state: &Model{ - Id: types.StringValue("pid,eu01,tid"), - ProjectId: types.StringValue("pid"), - Region: types.StringValue("eu01"), - RotateWhenChanged: types.MapNull(types.StringType), - }, - inputCreateTokenResponse: &modelserving.CreateTokenResponse{ - Token: &modelserving.TokenCreated{ - Id: utils.Ptr("tid"), - ValidUntil: utils.Ptr( - time.Date(2099, 1, 1, 0, 0, 0, 0, time.UTC), - ), - State: modelserving.TOKENCREATEDSTATE_ACTIVE.Ptr(), - Name: utils.Ptr("name"), - Description: utils.Ptr("desc"), - Region: utils.Ptr("eu01"), - Content: utils.Ptr("content"), - }, - }, - inputGetTokenResponse: &modelserving.GetTokenResponse{ - Token: &modelserving.Token{ - State: modelserving.TOKENSTATE_ACTIVE.Ptr(), - }, - }, - expected: Model{ - Id: types.StringValue("pid,eu01,tid"), - ProjectId: types.StringValue("pid"), - Region: types.StringValue("eu01"), - TokenId: types.StringValue("tid"), - Name: types.StringValue("name"), - Description: types.StringValue("desc"), - State: types.StringValue(string(modelserving.TOKENSTATE_ACTIVE)), - ValidUntil: types.StringValue("2099-01-01T00:00:00Z"), - Token: types.StringValue("content"), - RotateWhenChanged: types.MapNull(types.StringType), - }, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - t.Parallel() - - err := mapCreateResponse( - tt.inputCreateTokenResponse, - tt.inputGetTokenResponse, - tt.state, - "eu01", - ) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - - if tt.isValid { - diff := cmp.Diff(tt.state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - input *Model - expected *modelserving.CreateTokenPayload - isValid bool - }{ - { - description: "should error on nil input", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "should convert correctly", - input: &Model{ - Name: types.StringValue("name"), - Description: types.StringValue("desc"), - TTLDuration: types.StringValue("1h"), - }, - expected: &modelserving.CreateTokenPayload{ - Name: utils.Ptr("name"), - Description: utils.Ptr("desc"), - TtlDuration: utils.Ptr("1h"), - }, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - t.Parallel() - - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - t.Parallel() - - tests := []struct { - description string - input *Model - expected *modelserving.PartialUpdateTokenPayload - isValid bool - }{ - { - description: "should error on nil input", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "should convert correctly", - input: &Model{ - Name: types.StringValue("name"), - Description: types.StringValue("desc"), - }, - expected: &modelserving.PartialUpdateTokenPayload{ - Name: utils.Ptr("name"), - Description: utils.Ptr("desc"), - }, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - t.Parallel() - - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/modelserving/utils/util.go b/stackit/internal/services/modelserving/utils/util.go deleted file mode 100644 index 5b2bc505..00000000 --- a/stackit/internal/services/modelserving/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/modelserving" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *modelserving.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ModelServingCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ModelServingCustomEndpoint)) - } - apiClient, err := modelserving.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/modelserving/utils/util_test.go b/stackit/internal/services/modelserving/utils/util_test.go deleted file mode 100644 index e03889e2..00000000 --- a/stackit/internal/services/modelserving/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/modelserving" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://modelserving-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *modelserving.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *modelserving.APIClient { - apiClient, err := modelserving.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ModelServingCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *modelserving.APIClient { - apiClient, err := modelserving.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/mongodbflex/instance/datasource.go b/stackit/internal/services/mongodbflex/instance/datasource.go deleted file mode 100644 index 84d7563c..00000000 --- a/stackit/internal/services/mongodbflex/instance/datasource.go +++ /dev/null @@ -1,263 +0,0 @@ -package mongodbflex - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *mongodbflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mongodbflex_instance" -} - -// Configure adds the provider configured client to the data source. -func (d *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mongodbflexUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "MongoDB Flex instance client configured") -} - -// Schema defines the schema for the data source. -func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "MongoDB Flex instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source ID. It is structured as \"`project_id`,`region`,`instance_id`\".", - "instance_id": "ID of the MongoDB Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acl": "The Access Control List (ACL) for the MongoDB Flex instance.", - "backup_schedule": `The backup schedule. Should follow the cron scheduling system format (e.g. "0 0 * * *").`, - "options": "Custom parameters for the MongoDB Flex instance.", - "type": "Type of the MongoDB Flex instance.", - "snapshot_retention_days": "The number of days that continuous backups (controlled via the `backup_schedule`) will be retained.", - "daily_snapshot_retention_days": "The number of days that daily backups will be retained.", - "weekly_snapshot_retention_weeks": "The number of weeks that weekly backups will be retained.", - "monthly_snapshot_retention_months": "The number of months that monthly backups will be retained.", - "point_in_time_window_hours": "The number of hours back in time the point-in-time recovery feature will be able to recover.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Computed: true, - }, - "backup_schedule": schema.StringAttribute{ - Description: descriptions["backup_schedule"], - Computed: true, - }, - "flavor": schema.SingleNestedAttribute{ - Computed: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - }, - "description": schema.StringAttribute{ - Computed: true, - }, - "cpu": schema.Int64Attribute{ - Computed: true, - }, - "ram": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "replicas": schema.Int64Attribute{ - Computed: true, - }, - "storage": schema.SingleNestedAttribute{ - Computed: true, - Attributes: map[string]schema.Attribute{ - "class": schema.StringAttribute{ - Computed: true, - }, - "size": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "version": schema.StringAttribute{ - Computed: true, - }, - "options": schema.SingleNestedAttribute{ - Description: descriptions["options"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: descriptions["type"], - Computed: true, - }, - "snapshot_retention_days": schema.Int64Attribute{ - Description: descriptions["snapshot_retention_days"], - Computed: true, - }, - "daily_snapshot_retention_days": schema.Int64Attribute{ - Description: descriptions["daily_snapshot_retention_days"], - Computed: true, - }, - "weekly_snapshot_retention_weeks": schema.Int64Attribute{ - Description: descriptions["weekly_snapshot_retention_weeks"], - Computed: true, - }, - "monthly_snapshot_retention_months": schema.Int64Attribute{ - Description: descriptions["monthly_snapshot_retention_months"], - Computed: true, - }, - "point_in_time_window_hours": schema.Int64Attribute{ - Description: descriptions["point_in_time_window_hours"], - Computed: true, - }, - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - instanceResp, err := d.client.GetInstance(ctx, projectId, instanceId, region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err = mapFields(ctx, instanceResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex instance read") -} diff --git a/stackit/internal/services/mongodbflex/instance/resource.go b/stackit/internal/services/mongodbflex/instance/resource.go deleted file mode 100644 index 3f860037..00000000 --- a/stackit/internal/services/mongodbflex/instance/resource.go +++ /dev/null @@ -1,1081 +0,0 @@ -package mongodbflex - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strconv" - "strings" - "time" - - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "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" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} - _ resource.ResourceWithModifyPlan = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - ACL types.List `tfsdk:"acl"` - BackupSchedule types.String `tfsdk:"backup_schedule"` - Flavor types.Object `tfsdk:"flavor"` - Replicas types.Int64 `tfsdk:"replicas"` - Storage types.Object `tfsdk:"storage"` - Version types.String `tfsdk:"version"` - Options types.Object `tfsdk:"options"` - Region types.String `tfsdk:"region"` -} - -// Struct corresponding to Model.Flavor -type flavorModel struct { - Id types.String `tfsdk:"id"` - Description types.String `tfsdk:"description"` - CPU types.Int64 `tfsdk:"cpu"` - RAM types.Int64 `tfsdk:"ram"` -} - -// Types corresponding to flavorModel -var flavorTypes = map[string]attr.Type{ - "id": basetypes.StringType{}, - "description": basetypes.StringType{}, - "cpu": basetypes.Int64Type{}, - "ram": basetypes.Int64Type{}, -} - -// Struct corresponding to Model.Storage -type storageModel struct { - Class types.String `tfsdk:"class"` - Size types.Int64 `tfsdk:"size"` -} - -// Types corresponding to storageModel -var storageTypes = map[string]attr.Type{ - "class": basetypes.StringType{}, - "size": basetypes.Int64Type{}, -} - -// Struct corresponding to Model.Options -type optionsModel struct { - Type types.String `tfsdk:"type"` - SnapshotRetentionDays types.Int64 `tfsdk:"snapshot_retention_days"` - PointInTimeWindowHours types.Int64 `tfsdk:"point_in_time_window_hours"` - DailySnapshotRetentionDays types.Int64 `tfsdk:"daily_snapshot_retention_days"` - WeeklySnapshotRetentionWeeks types.Int64 `tfsdk:"weekly_snapshot_retention_weeks"` - MonthlySnapshotRetentionMonths types.Int64 `tfsdk:"monthly_snapshot_retention_months"` -} - -// Types corresponding to optionsModel -var optionsTypes = map[string]attr.Type{ - "type": basetypes.StringType{}, - "snapshot_retention_days": basetypes.Int64Type{}, - "point_in_time_window_hours": basetypes.Int64Type{}, - "daily_snapshot_retention_days": basetypes.Int64Type{}, - "weekly_snapshot_retention_weeks": basetypes.Int64Type{}, - "monthly_snapshot_retention_months": basetypes.Int64Type{}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *mongodbflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mongodbflex_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mongodbflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "MongoDB Flex instance client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - typeOptions := []string{"Replica", "Sharded", "Single"} - - descriptions := map[string]string{ - "main": "MongoDB Flex instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\".", - "instance_id": "ID of the MongoDB Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acl": "The Access Control List (ACL) for the MongoDB Flex instance.", - "backup_schedule": `The backup schedule. Should follow the cron scheduling system format (e.g. "0 0 * * *").`, - "options": "Custom parameters for the MongoDB Flex instance.", - "type": fmt.Sprintf("Type of the MongoDB Flex instance. %s", utils.FormatPossibleValues(typeOptions...)), - "snapshot_retention_days": "The number of days that continuous backups (controlled via the `backup_schedule`) will be retained.", - "daily_snapshot_retention_days": "The number of days that daily backups will be retained.", - "weekly_snapshot_retention_weeks": "The number of weeks that weekly backups will be retained.", - "monthly_snapshot_retention_months": "The number of months that monthly backups will be retained.", - "point_in_time_window_hours": "The number of hours back in time the point-in-time recovery feature will be able to recover.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-z]([-a-z0-9]*[a-z0-9])?$"), - "must start with a letter, must have lower case letters, numbers or hyphens, and no hyphen at the end", - ), - }, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Required: true, - }, - "backup_schedule": schema.StringAttribute{ - Description: descriptions["backup_schedule"], - Required: true, - }, - "flavor": schema.SingleNestedAttribute{ - Required: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - }, - "description": schema.StringAttribute{ - Computed: true, - }, - "cpu": schema.Int64Attribute{ - Required: true, - }, - "ram": schema.Int64Attribute{ - Required: true, - }, - }, - }, - "replicas": schema.Int64Attribute{ - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - }, - "storage": schema.SingleNestedAttribute{ - Required: true, - Attributes: map[string]schema.Attribute{ - "class": schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "size": schema.Int64Attribute{ - Required: true, - }, - }, - }, - "version": schema.StringAttribute{ - Required: true, - }, - "options": schema.SingleNestedAttribute{ - Required: true, - Attributes: map[string]schema.Attribute{ - "type": schema.StringAttribute{ - Description: descriptions["type"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "snapshot_retention_days": schema.Int64Attribute{ - Description: descriptions["snapshot_retention_days"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "daily_snapshot_retention_days": schema.Int64Attribute{ - Description: descriptions["daily_snapshot_retention_days"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "weekly_snapshot_retention_weeks": schema.Int64Attribute{ - Description: descriptions["weekly_snapshot_retention_weeks"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "monthly_snapshot_retention_months": schema.Int64Attribute{ - Description: descriptions["monthly_snapshot_retention_months"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "point_in_time_window_hours": schema.Int64Attribute{ - Description: descriptions["point_in_time_window_hours"], - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - err := loadFlavorId(ctx, r.client, &model, flavor, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err)) - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, acl, flavor, storage, options) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId, region).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if createResp == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "API response is empty") - return - } - if createResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "API response does not contain instance id") - return - } - instanceId := *createResp.Id - ctx = tflog.SetField(ctx, "instance_id", instanceId) - diags = resp.State.SetAttribute(ctx, path.Root("project_id"), projectId) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - diags = resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - backupScheduleOptionsPayload, err := toUpdateBackupScheduleOptionsPayload(ctx, &model, options) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - backupScheduleOptions, err := r.client.UpdateBackupSchedule(ctx, projectId, instanceId, region).UpdateBackupSchedulePayload(*backupScheduleOptionsPayload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Updating options: %v", err)) - return - } - - err = mapOptions(&model, options, backupScheduleOptions) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId, region).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, instanceResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - err := loadFlavorId(ctx, r.client, &model, flavor, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err)) - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, acl, flavor, storage, options) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - _, err = r.client.PartialUpdateInstance(ctx, projectId, instanceId, region).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - backupScheduleOptionsPayload, err := toUpdateBackupScheduleOptionsPayload(ctx, &model, options) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - backupScheduleOptions, err := r.client.UpdateBackupSchedule(ctx, projectId, instanceId, region).UpdateBackupSchedulePayload(*backupScheduleOptionsPayload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Updating options: %v", err)) - return - } - - err = mapOptions(&model, options, backupScheduleOptions) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API response: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - - // This is needed because the waiter is currently not working properly - // After the get request returns 404 (instance is deleted), creating a new instance with the same name still fails for a short period of time - time.Sleep(30 * time.Second) - - tflog.Info(ctx, "MongoDB Flex instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - tflog.Info(ctx, "MongoDB Flex instance state imported") -} - -func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *Model, flavor *flavorModel, storage *storageModel, options *optionsModel, region string) error { - if resp == nil { - return fmt.Errorf("response input is nil") - } - if resp.Item == nil { - return fmt.Errorf("no instance provided") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - instance := resp.Item - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.Id != nil { - instanceId = *instance.Id - } else { - return fmt.Errorf("instance id not present") - } - - var aclList basetypes.ListValue - var diags diag.Diagnostics - if instance.Acl == nil || instance.Acl.Items == nil { - aclList = types.ListNull(types.StringType) - } else { - respACL := *instance.Acl.Items - modelACL, err := utils.ListValuetoStringSlice(model.ACL) - if err != nil { - return err - } - - reconciledACL := utils.ReconcileStringSlices(modelACL, respACL) - - aclList, diags = types.ListValueFrom(ctx, types.StringType, reconciledACL) - if diags.HasError() { - return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) - } - } - - var flavorValues map[string]attr.Value - if instance.Flavor == nil { - flavorValues = map[string]attr.Value{ - "id": flavor.Id, - "description": flavor.Description, - "cpu": flavor.CPU, - "ram": flavor.RAM, - } - } else { - flavorValues = map[string]attr.Value{ - "id": types.StringValue(*instance.Flavor.Id), - "description": types.StringValue(*instance.Flavor.Description), - "cpu": types.Int64PointerValue(instance.Flavor.Cpu), - "ram": types.Int64PointerValue(instance.Flavor.Memory), - } - } - flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues) - if diags.HasError() { - return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags)) - } - - var storageValues map[string]attr.Value - if instance.Storage == nil { - storageValues = map[string]attr.Value{ - "class": storage.Class, - "size": storage.Size, - } - } else { - storageValues = map[string]attr.Value{ - "class": types.StringValue(*instance.Storage.Class), - "size": types.Int64PointerValue(instance.Storage.Size), - } - } - storageObject, diags := types.ObjectValue(storageTypes, storageValues) - if diags.HasError() { - return fmt.Errorf("creating storage: %w", core.DiagsToError(diags)) - } - - var optionsValues map[string]attr.Value - if instance.Options == nil { - optionsValues = map[string]attr.Value{ - "type": options.Type, - "snapshot_retention_days": types.Int64Null(), - "daily_snapshot_retention_days": types.Int64Null(), - "weekly_snapshot_retention_weeks": types.Int64Null(), - "monthly_snapshot_retention_months": types.Int64Null(), - "point_in_time_window_hours": types.Int64Null(), - } - } else { - snapshotRetentionDaysStr := (*instance.Options)["snapshotRetentionDays"] - snapshotRetentionDays, err := strconv.ParseInt(snapshotRetentionDaysStr, 10, 64) - if err != nil { - return fmt.Errorf("parse snapshot retention days: %w", err) - } - dailySnapshotRetentionDaysStr := (*instance.Options)["dailySnapshotRetentionDays"] - dailySnapshotRetentionDays, err := strconv.ParseInt(dailySnapshotRetentionDaysStr, 10, 64) - if err != nil { - return fmt.Errorf("parse daily snapshot retention days: %w", err) - } - weeklySnapshotRetentionWeeksStr := (*instance.Options)["weeklySnapshotRetentionWeeks"] - weeklySnapshotRetentionWeeks, err := strconv.ParseInt(weeklySnapshotRetentionWeeksStr, 10, 64) - if err != nil { - return fmt.Errorf("parse weekly snapshot retention weeks: %w", err) - } - monthlySnapshotRetentionMonthsStr := (*instance.Options)["monthlySnapshotRetentionMonths"] - monthlySnapshotRetentionMonths, err := strconv.ParseInt(monthlySnapshotRetentionMonthsStr, 10, 64) - if err != nil { - return fmt.Errorf("parse monthly snapshot retention months: %w", err) - } - pointInTimeWindowHoursStr := (*instance.Options)["pointInTimeWindowHours"] - pointInTimeWindowHours, err := strconv.ParseInt(pointInTimeWindowHoursStr, 10, 64) - if err != nil { - return fmt.Errorf("parse point in time window hours: %w", err) - } - - optionsValues = map[string]attr.Value{ - "type": types.StringValue((*instance.Options)["type"]), - "snapshot_retention_days": types.Int64Value(snapshotRetentionDays), - "daily_snapshot_retention_days": types.Int64Value(dailySnapshotRetentionDays), - "weekly_snapshot_retention_weeks": types.Int64Value(weeklySnapshotRetentionWeeks), - "monthly_snapshot_retention_months": types.Int64Value(monthlySnapshotRetentionMonths), - "point_in_time_window_hours": types.Int64Value(pointInTimeWindowHours), - } - } - optionsObject, diags := types.ObjectValue(optionsTypes, optionsValues) - if diags.HasError() { - return fmt.Errorf("creating options: %w", core.DiagsToError(diags)) - } - - simplifiedModelBackupSchedule := utils.SimplifyBackupSchedule(model.BackupSchedule.ValueString()) - // If the value returned by the API is different from the one in the model after simplification, - // we update the model so that it causes an error in Terraform - if simplifiedModelBackupSchedule != types.StringPointerValue(instance.BackupSchedule).ValueString() { - model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule) - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, instanceId) - model.Region = types.StringValue(region) - model.InstanceId = types.StringValue(instanceId) - model.Name = types.StringPointerValue(instance.Name) - model.ACL = aclList - model.Flavor = flavorObject - model.Replicas = types.Int64PointerValue(instance.Replicas) - model.Storage = storageObject - model.Version = types.StringPointerValue(instance.Version) - model.Options = optionsObject - return nil -} - -func mapOptions(model *Model, options *optionsModel, backupScheduleOptions *mongodbflex.BackupSchedule) error { - var optionsValues map[string]attr.Value - if backupScheduleOptions == nil { - optionsValues = map[string]attr.Value{ - "type": options.Type, - "snapshot_retention_days": types.Int64Null(), - "daily_snapshot_retention_days": types.Int64Null(), - "weekly_snapshot_retention_weeks": types.Int64Null(), - "monthly_snapshot_retention_months": types.Int64Null(), - "point_in_time_window_hours": types.Int64Null(), - } - } else { - optionsValues = map[string]attr.Value{ - "type": options.Type, - "snapshot_retention_days": types.Int64Value(*backupScheduleOptions.SnapshotRetentionDays), - "daily_snapshot_retention_days": types.Int64Value(*backupScheduleOptions.DailySnapshotRetentionDays), - "weekly_snapshot_retention_weeks": types.Int64Value(*backupScheduleOptions.WeeklySnapshotRetentionWeeks), - "monthly_snapshot_retention_months": types.Int64Value(*backupScheduleOptions.MonthlySnapshotRetentionMonths), - "point_in_time_window_hours": types.Int64Value(*backupScheduleOptions.PointInTimeWindowHours), - } - } - optionsTF, diags := types.ObjectValue(optionsTypes, optionsValues) - if diags.HasError() { - return fmt.Errorf("creating options: %w", core.DiagsToError(diags)) - } - model.Options = optionsTF - return nil -} - -func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *storageModel, options *optionsModel) (*mongodbflex.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if acl == nil { - return nil, fmt.Errorf("nil acl") - } - if flavor == nil { - return nil, fmt.Errorf("nil flavor") - } - if storage == nil { - return nil, fmt.Errorf("nil storage") - } - if options == nil { - return nil, fmt.Errorf("nil options") - } - - payloadOptions := make(map[string]string) - if options.Type.ValueString() != "" { - payloadOptions["type"] = options.Type.ValueString() - } - - return &mongodbflex.CreateInstancePayload{ - Acl: &mongodbflex.CreateInstancePayloadAcl{ - Items: &acl, - }, - BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule), - FlavorId: conversion.StringValueToPointer(flavor.Id), - Name: conversion.StringValueToPointer(model.Name), - Replicas: conversion.Int64ValueToPointer(model.Replicas), - Storage: &mongodbflex.Storage{ - Class: conversion.StringValueToPointer(storage.Class), - Size: conversion.Int64ValueToPointer(storage.Size), - }, - Version: conversion.StringValueToPointer(model.Version), - Options: &payloadOptions, - }, nil -} - -func toUpdatePayload(model *Model, acl []string, flavor *flavorModel, storage *storageModel, options *optionsModel) (*mongodbflex.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if acl == nil { - return nil, fmt.Errorf("nil acl") - } - if flavor == nil { - return nil, fmt.Errorf("nil flavor") - } - if storage == nil { - return nil, fmt.Errorf("nil storage") - } - if options == nil { - return nil, fmt.Errorf("nil options") - } - - payloadOptions := make(map[string]string) - if options.Type.ValueString() != "" { - payloadOptions["type"] = options.Type.ValueString() - } - - return &mongodbflex.PartialUpdateInstancePayload{ - Acl: &mongodbflex.ACL{ - Items: &acl, - }, - BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule), - FlavorId: conversion.StringValueToPointer(flavor.Id), - Name: conversion.StringValueToPointer(model.Name), - Replicas: conversion.Int64ValueToPointer(model.Replicas), - Storage: &mongodbflex.Storage{ - Class: conversion.StringValueToPointer(storage.Class), - Size: conversion.Int64ValueToPointer(storage.Size), - }, - Version: conversion.StringValueToPointer(model.Version), - Options: &payloadOptions, - }, nil -} - -func toUpdateBackupScheduleOptionsPayload(ctx context.Context, model *Model, configuredOptions *optionsModel) (*mongodbflex.UpdateBackupSchedulePayload, error) { - if model == nil || configuredOptions == nil { - return nil, nil - } - - var currOptions = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags := model.Options.As(ctx, currOptions, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("map current options: %w", core.DiagsToError(diags)) - } - } - - backupSchedule := conversion.StringValueToPointer(model.BackupSchedule) - - snapshotRetentionDays := conversion.Int64ValueToPointer(configuredOptions.SnapshotRetentionDays) - if snapshotRetentionDays == nil { - snapshotRetentionDays = conversion.Int64ValueToPointer(currOptions.SnapshotRetentionDays) - } - - dailySnapshotRetentionDays := conversion.Int64ValueToPointer(configuredOptions.DailySnapshotRetentionDays) - if dailySnapshotRetentionDays == nil { - dailySnapshotRetentionDays = conversion.Int64ValueToPointer(currOptions.DailySnapshotRetentionDays) - } - - weeklySnapshotRetentionWeeks := conversion.Int64ValueToPointer(configuredOptions.WeeklySnapshotRetentionWeeks) - if weeklySnapshotRetentionWeeks == nil { - weeklySnapshotRetentionWeeks = conversion.Int64ValueToPointer(currOptions.WeeklySnapshotRetentionWeeks) - } - - monthlySnapshotRetentionMonths := conversion.Int64ValueToPointer(configuredOptions.MonthlySnapshotRetentionMonths) - if monthlySnapshotRetentionMonths == nil { - monthlySnapshotRetentionMonths = conversion.Int64ValueToPointer(currOptions.MonthlySnapshotRetentionMonths) - } - - pointInTimeWindowHours := conversion.Int64ValueToPointer(configuredOptions.PointInTimeWindowHours) - if pointInTimeWindowHours == nil { - pointInTimeWindowHours = conversion.Int64ValueToPointer(currOptions.PointInTimeWindowHours) - } - - return &mongodbflex.UpdateBackupSchedulePayload{ - // This is a PUT endpoint and all fields are required - BackupSchedule: backupSchedule, - SnapshotRetentionDays: snapshotRetentionDays, - DailySnapshotRetentionDays: dailySnapshotRetentionDays, - WeeklySnapshotRetentionWeeks: weeklySnapshotRetentionWeeks, - MonthlySnapshotRetentionMonths: monthlySnapshotRetentionMonths, - PointInTimeWindowHours: pointInTimeWindowHours, - }, nil -} - -type mongoDBFlexClient interface { - ListFlavorsExecute(ctx context.Context, projectId, region string) (*mongodbflex.ListFlavorsResponse, error) -} - -func loadFlavorId(ctx context.Context, client mongoDBFlexClient, model *Model, flavor *flavorModel, region string) error { - if model == nil { - return fmt.Errorf("nil model") - } - if flavor == nil { - return fmt.Errorf("nil flavor") - } - cpu := conversion.Int64ValueToPointer(flavor.CPU) - if cpu == nil { - return fmt.Errorf("nil CPU") - } - ram := conversion.Int64ValueToPointer(flavor.RAM) - if ram == nil { - return fmt.Errorf("nil RAM") - } - - projectId := model.ProjectId.ValueString() - res, err := client.ListFlavorsExecute(ctx, projectId, region) - if err != nil { - return fmt.Errorf("listing mongodbflex flavors: %w", err) - } - - avl := "" - if res.Flavors == nil { - return fmt.Errorf("finding flavors for project %s", projectId) - } - for _, f := range *res.Flavors { - if f.Id == nil || f.Cpu == nil || f.Memory == nil { - continue - } - if *f.Cpu == *cpu && *f.Memory == *ram { - flavor.Id = types.StringValue(*f.Id) - flavor.Description = types.StringValue(*f.Description) - break - } - avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory) - } - if flavor.Id.ValueString() == "" { - return fmt.Errorf("couldn't find flavor, available specs are:%s", avl) - } - - return nil -} diff --git a/stackit/internal/services/mongodbflex/instance/resource_test.go b/stackit/internal/services/mongodbflex/instance/resource_test.go deleted file mode 100644 index 732b5038..00000000 --- a/stackit/internal/services/mongodbflex/instance/resource_test.go +++ /dev/null @@ -1,1093 +0,0 @@ -package mongodbflex - -import ( - "context" - "fmt" - "testing" - - "github.com/google/uuid" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" -) - -const ( - testRegion = "eu02" -) - -var ( - projectId = uuid.NewString() - instanceId = uuid.NewString() -) - -type mongoDBFlexClientMocked struct { - returnError bool - listFlavorsResp *mongodbflex.ListFlavorsResponse -} - -func (c *mongoDBFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*mongodbflex.ListFlavorsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("get flavors failed") - } - - return c.listFlavorsResp, nil -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *mongodbflex.InstanceResponse - flavor *flavorModel - storage *storageModel - options *optionsModel - region string - expected Model - isValid bool - }{ - { - "default_values", - Model{ - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - }, - &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{}, - }, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, testRegion, instanceId)), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Name: types.StringNull(), - ACL: types.ListNull(types.StringType), - BackupSchedule: types.StringNull(), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Null(), - "ram": types.Int64Null(), - }), - Replicas: types.Int64Null(), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringNull(), - "size": types.Int64Null(), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringNull(), - "snapshot_retention_days": types.Int64Null(), - "daily_snapshot_retention_days": types.Int64Null(), - "weekly_snapshot_retention_weeks": types.Int64Null(), - "monthly_snapshot_retention_months": types.Int64Null(), - "point_in_time_window_hours": types.Int64Null(), - }), - Version: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - Model{ - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - }, - &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{ - Acl: &mongodbflex.ACL{ - Items: &[]string{ - "ip1", - "ip2", - "", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: &mongodbflex.Flavor{ - Cpu: utils.Ptr(int64(12)), - Description: utils.Ptr("description"), - Id: utils.Ptr("flavor_id"), - Memory: utils.Ptr(int64(34)), - }, - Id: utils.Ptr(instanceId), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: mongodbflex.INSTANCESTATUS_READY.Ptr(), - Storage: &mongodbflex.Storage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(78)), - }, - Options: &map[string]string{ - "type": "type", - "snapshotRetentionDays": "5", - "dailySnapshotRetentionDays": "6", - "weeklySnapshotRetentionWeeks": "7", - "monthlySnapshotRetentionMonths": "8", - "pointInTimeWindowHours": "9", - }, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, testRegion, instanceId)), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip1"), - types.StringValue("ip2"), - types.StringValue(""), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringValue("flavor_id"), - "description": types.StringValue("description"), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringValue("type"), - "snapshot_retention_days": types.Int64Value(5), - "daily_snapshot_retention_days": types.Int64Value(6), - "weekly_snapshot_retention_weeks": types.Int64Value(7), - "monthly_snapshot_retention_months": types.Int64Value(8), - "point_in_time_window_hours": types.Int64Value(9), - }), - Region: types.StringValue(testRegion), - Version: types.StringValue("version"), - }, - true, - }, - { - "simple_values_no_flavor_and_storage", - Model{ - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - }, - &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{ - Acl: &mongodbflex.ACL{ - Items: &[]string{ - "ip1", - "ip2", - "", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: nil, - Id: utils.Ptr(instanceId), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: mongodbflex.INSTANCESTATUS_READY.Ptr(), - Storage: nil, - Options: &map[string]string{ - "type": "type", - "snapshotRetentionDays": "5", - "dailySnapshotRetentionDays": "6", - "weeklySnapshotRetentionWeeks": "7", - "monthlySnapshotRetentionMonths": "8", - "pointInTimeWindowHours": "9", - }, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{ - CPU: types.Int64Value(12), - RAM: types.Int64Value(34), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(78), - }, - &optionsModel{ - Type: types.StringValue("type"), - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, testRegion, instanceId)), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip1"), - types.StringValue("ip2"), - types.StringValue(""), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringValue("type"), - "snapshot_retention_days": types.Int64Value(5), - "daily_snapshot_retention_days": types.Int64Value(6), - "weekly_snapshot_retention_weeks": types.Int64Value(7), - "monthly_snapshot_retention_months": types.Int64Value(8), - "point_in_time_window_hours": types.Int64Value(9), - }), - Region: types.StringValue(testRegion), - Version: types.StringValue("version"), - }, - true, - }, - { - "acls_unordered", - Model{ - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip2"), - types.StringValue(""), - types.StringValue("ip1"), - }), - }, - &mongodbflex.InstanceResponse{ - Item: &mongodbflex.Instance{ - Acl: &mongodbflex.ACL{ - Items: &[]string{ - "", - "ip1", - "ip2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: nil, - Id: utils.Ptr(instanceId), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: mongodbflex.INSTANCESTATUS_READY.Ptr(), - Storage: nil, - Options: &map[string]string{ - "type": "type", - "snapshotRetentionDays": "5", - "dailySnapshotRetentionDays": "6", - "weeklySnapshotRetentionWeeks": "7", - "monthlySnapshotRetentionMonths": "8", - "pointInTimeWindowHours": "9", - }, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{ - CPU: types.Int64Value(12), - RAM: types.Int64Value(34), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(78), - }, - &optionsModel{ - Type: types.StringValue("type"), - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, testRegion, instanceId)), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip2"), - types.StringValue(""), - types.StringValue("ip1"), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringValue("type"), - "snapshot_retention_days": types.Int64Value(5), - "daily_snapshot_retention_days": types.Int64Value(6), - "weekly_snapshot_retention_weeks": types.Int64Value(7), - "monthly_snapshot_retention_months": types.Int64Value(8), - "point_in_time_window_hours": types.Int64Value(9), - }), - Region: types.StringValue(testRegion), - Version: types.StringValue("version"), - }, - true, - }, - { - "nil_response", - Model{ - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - }, - nil, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - }, - &mongodbflex.InstanceResponse{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state, tt.flavor, tt.storage, tt.options, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapOptions(t *testing.T) { - tests := []struct { - description string - model *Model - options *optionsModel - backup *mongodbflex.BackupSchedule - expected *Model - isValid bool - }{ - { - "default_values", - &Model{}, - &optionsModel{}, - nil, - &Model{ - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringNull(), - "snapshot_retention_days": types.Int64Null(), - "daily_snapshot_retention_days": types.Int64Null(), - "weekly_snapshot_retention_weeks": types.Int64Null(), - "monthly_snapshot_retention_months": types.Int64Null(), - "point_in_time_window_hours": types.Int64Null(), - }), - }, - true, - }, - { - "simple_values", - &Model{}, - &optionsModel{ - Type: types.StringValue("type"), - }, - &mongodbflex.BackupSchedule{ - SnapshotRetentionDays: utils.Ptr(int64(1)), - DailySnapshotRetentionDays: utils.Ptr(int64(2)), - WeeklySnapshotRetentionWeeks: utils.Ptr(int64(3)), - MonthlySnapshotRetentionMonths: utils.Ptr(int64(4)), - PointInTimeWindowHours: utils.Ptr(int64(5)), - }, - &Model{ - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringValue("type"), - "snapshot_retention_days": types.Int64Value(1), - "daily_snapshot_retention_days": types.Int64Value(2), - "weekly_snapshot_retention_weeks": types.Int64Value(3), - "monthly_snapshot_retention_months": types.Int64Value(4), - "point_in_time_window_hours": types.Int64Value(5), - }), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapOptions(tt.model, tt.options, tt.backup) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.model, tt.expected, cmpopts.IgnoreFields(Model{}, "ACL", "Flavor", "Replicas", "Storage", "Version")) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputAcl []string - inputFlavor *flavorModel - inputStorage *storageModel - inputOptions *optionsModel - expected *mongodbflex.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - &mongodbflex.CreateInstancePayload{ - Acl: &mongodbflex.CreateInstancePayloadAcl{ - Items: &[]string{}, - }, - Storage: &mongodbflex.Storage{}, - Options: &map[string]string{}, - }, - true, - }, - { - "simple_values", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Name: types.StringValue("name"), - Replicas: types.Int64Value(12), - Version: types.StringValue("version"), - }, - []string{ - "ip_1", - "ip_2", - }, - &flavorModel{ - Id: types.StringValue("flavor_id"), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(34), - }, - &optionsModel{ - Type: types.StringValue("type"), - }, - &mongodbflex.CreateInstancePayload{ - Acl: &mongodbflex.CreateInstancePayloadAcl{ - Items: &[]string{ - "ip_1", - "ip_2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - FlavorId: utils.Ptr("flavor_id"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(12)), - Storage: &mongodbflex.Storage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(34)), - }, - Options: &map[string]string{"type": "type"}, - Version: utils.Ptr("version"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - BackupSchedule: types.StringNull(), - Name: types.StringNull(), - Replicas: types.Int64Value(2123456789), - Version: types.StringNull(), - }, - []string{ - "", - }, - &flavorModel{ - Id: types.StringNull(), - }, - &storageModel{ - Class: types.StringNull(), - Size: types.Int64Null(), - }, - &optionsModel{ - Type: types.StringNull(), - }, - &mongodbflex.CreateInstancePayload{ - Acl: &mongodbflex.CreateInstancePayloadAcl{ - Items: &[]string{ - "", - }, - }, - BackupSchedule: nil, - FlavorId: nil, - Name: nil, - Replicas: utils.Ptr(int64(2123456789)), - Storage: &mongodbflex.Storage{ - Class: nil, - Size: nil, - }, - Options: &map[string]string{}, - Version: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_acl", - &Model{}, - nil, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_flavor", - &Model{}, - []string{}, - nil, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_storage", - &Model{}, - []string{}, - &flavorModel{}, - nil, - &optionsModel{}, - nil, - false, - }, - { - "nil_options", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputAcl, tt.inputFlavor, tt.inputStorage, tt.inputOptions) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputAcl []string - inputFlavor *flavorModel - inputStorage *storageModel - inputOptions *optionsModel - expected *mongodbflex.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - &mongodbflex.PartialUpdateInstancePayload{ - Acl: &mongodbflex.ACL{ - Items: &[]string{}, - }, - Storage: &mongodbflex.Storage{}, - Options: &map[string]string{}, - }, - true, - }, - { - "simple_values", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Name: types.StringValue("name"), - Replicas: types.Int64Value(12), - Version: types.StringValue("version"), - }, - []string{ - "ip_1", - "ip_2", - }, - &flavorModel{ - Id: types.StringValue("flavor_id"), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(34), - }, - &optionsModel{ - Type: types.StringValue("type"), - }, - &mongodbflex.PartialUpdateInstancePayload{ - Acl: &mongodbflex.ACL{ - Items: &[]string{ - "ip_1", - "ip_2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - FlavorId: utils.Ptr("flavor_id"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(12)), - Storage: &mongodbflex.Storage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(34)), - }, - Options: &map[string]string{"type": "type"}, - Version: utils.Ptr("version"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - BackupSchedule: types.StringNull(), - Name: types.StringNull(), - Replicas: types.Int64Value(2123456789), - Version: types.StringNull(), - }, - []string{ - "", - }, - &flavorModel{ - Id: types.StringNull(), - }, - &storageModel{ - Class: types.StringNull(), - Size: types.Int64Null(), - }, - &optionsModel{ - Type: types.StringNull(), - }, - &mongodbflex.PartialUpdateInstancePayload{ - Acl: &mongodbflex.ACL{ - Items: &[]string{ - "", - }, - }, - BackupSchedule: nil, - FlavorId: nil, - Name: nil, - Replicas: utils.Ptr(int64(2123456789)), - Storage: &mongodbflex.Storage{ - Class: nil, - Size: nil, - }, - Options: &map[string]string{}, - Version: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_acl", - &Model{}, - nil, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_flavor", - &Model{}, - []string{}, - nil, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_storage", - &Model{}, - []string{}, - &flavorModel{}, - nil, - &optionsModel{}, - nil, - false, - }, - { - "nil_options", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputAcl, tt.inputFlavor, tt.inputStorage, tt.inputOptions) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdateBackupScheduleOptionsPayload(t *testing.T) { - tests := []struct { - description string - model *Model - configuredOptions *optionsModel - expected *mongodbflex.UpdateBackupSchedulePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &optionsModel{}, - &mongodbflex.UpdateBackupSchedulePayload{ - BackupSchedule: nil, - SnapshotRetentionDays: nil, - DailySnapshotRetentionDays: nil, - WeeklySnapshotRetentionWeeks: nil, - MonthlySnapshotRetentionMonths: nil, - PointInTimeWindowHours: nil, - }, - true, - }, - { - "config values override current values in model", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringValue("type"), - "snapshot_retention_days": types.Int64Value(1), - "daily_snapshot_retention_days": types.Int64Value(2), - "weekly_snapshot_retention_weeks": types.Int64Value(3), - "monthly_snapshot_retention_months": types.Int64Value(4), - "point_in_time_window_hours": types.Int64Value(5), - }), - }, - &optionsModel{ - SnapshotRetentionDays: types.Int64Value(6), - DailySnapshotRetentionDays: types.Int64Value(7), - WeeklySnapshotRetentionWeeks: types.Int64Value(8), - MonthlySnapshotRetentionMonths: types.Int64Value(9), - PointInTimeWindowHours: types.Int64Value(10), - }, - &mongodbflex.UpdateBackupSchedulePayload{ - BackupSchedule: utils.Ptr("schedule"), - SnapshotRetentionDays: utils.Ptr(int64(6)), - DailySnapshotRetentionDays: utils.Ptr(int64(7)), - WeeklySnapshotRetentionWeeks: utils.Ptr(int64(8)), - MonthlySnapshotRetentionMonths: utils.Ptr(int64(9)), - PointInTimeWindowHours: utils.Ptr(int64(10)), - }, - true, - }, - { - "current values in model fill in missing values in config", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "type": types.StringValue("type"), - "snapshot_retention_days": types.Int64Value(1), - "daily_snapshot_retention_days": types.Int64Value(2), - "weekly_snapshot_retention_weeks": types.Int64Value(3), - "monthly_snapshot_retention_months": types.Int64Value(4), - "point_in_time_window_hours": types.Int64Value(5), - }), - }, - &optionsModel{ - SnapshotRetentionDays: types.Int64Value(6), - DailySnapshotRetentionDays: types.Int64Value(7), - WeeklySnapshotRetentionWeeks: types.Int64Null(), - MonthlySnapshotRetentionMonths: types.Int64Null(), - PointInTimeWindowHours: types.Int64Null(), - }, - &mongodbflex.UpdateBackupSchedulePayload{ - BackupSchedule: utils.Ptr("schedule"), - SnapshotRetentionDays: utils.Ptr(int64(6)), - DailySnapshotRetentionDays: utils.Ptr(int64(7)), - WeeklySnapshotRetentionWeeks: utils.Ptr(int64(3)), - MonthlySnapshotRetentionMonths: utils.Ptr(int64(4)), - PointInTimeWindowHours: utils.Ptr(int64(5)), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - BackupSchedule: types.StringNull(), - }, - &optionsModel{ - SnapshotRetentionDays: types.Int64Null(), - DailySnapshotRetentionDays: types.Int64Null(), - WeeklySnapshotRetentionWeeks: types.Int64Null(), - MonthlySnapshotRetentionMonths: types.Int64Null(), - PointInTimeWindowHours: types.Int64Null(), - }, - &mongodbflex.UpdateBackupSchedulePayload{ - BackupSchedule: nil, - SnapshotRetentionDays: nil, - DailySnapshotRetentionDays: nil, - WeeklySnapshotRetentionWeeks: nil, - MonthlySnapshotRetentionMonths: nil, - PointInTimeWindowHours: nil, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdateBackupScheduleOptionsPayload(context.Background(), tt.model, tt.configuredOptions) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestLoadFlavorId(t *testing.T) { - tests := []struct { - description string - inputFlavor *flavorModel - mockedResp *mongodbflex.ListFlavorsResponse - expected *flavorModel - getFlavorsFails bool - isValid bool - }{ - { - "ok_flavor", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(2)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - }, - }, - &flavorModel{ - Id: types.StringValue("fid-1"), - Description: types.StringValue("description"), - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - true, - }, - { - "ok_flavor_2", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(2)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - { - Id: utils.Ptr("fid-2"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - &flavorModel{ - Id: types.StringValue("fid-1"), - Description: types.StringValue("description"), - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - true, - }, - { - "no_matching_flavor", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - { - Id: utils.Ptr("fid-2"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - false, - }, - { - "nil_response", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &mongodbflex.ListFlavorsResponse{}, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - false, - }, - { - "error_response", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &mongodbflex.ListFlavorsResponse{}, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - true, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &mongoDBFlexClientMocked{ - returnError: tt.getFlavorsFails, - listFlavorsResp: tt.mockedResp, - } - model := &Model{ - ProjectId: types.StringValue(projectId), - } - flavorModel := &flavorModel{ - CPU: tt.inputFlavor.CPU, - RAM: tt.inputFlavor.RAM, - } - err := loadFlavorId(context.Background(), client, model, flavorModel, testRegion) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(flavorModel, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go b/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go deleted file mode 100644 index 0ab9598f..00000000 --- a/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go +++ /dev/null @@ -1,346 +0,0 @@ -package mongodbflex_test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// Instance resource data -var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)), - "acl": "192.168.0.0/16", - "flavor_cpu": "2", - "flavor_ram": "4", - "flavor_description": "Small, Compute optimized", - "replicas": "3", - "storage_class": "premium-perf2-mongodb", - "storage_size": "10", - "version": "7.0", - "version_updated": "8.0", - "options_type": "Replica", - "flavor_id": "2.4", - "backup_schedule": "00 6 * * *", - "backup_schedule_updated": "00 12 * * *", - "backup_schedule_read": "0 6 * * *", - "snapshot_retention_days": "4", - "snapshot_retention_days_updated": "3", - "daily_snapshot_retention_days": "1", - "point_in_time_window_hours": "30", -} - -// User resource data -var userResource = map[string]string{ - "username": fmt.Sprintf("tf-acc-user-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlpha)), - "role": "read", - "database": "default", - "project_id": instanceResource["project_id"], -} - -func configResources(version, backupSchedule, snapshotRetentionDays string) string { - return fmt.Sprintf(` - %s - - resource "stackit_mongodbflex_instance" "instance" { - project_id = "%s" - name = "%s" - acl = ["%s"] - flavor = { - cpu = %s - ram = %s - } - replicas = %s - storage = { - class = "%s" - size = %s - } - version = "%s" - options = { - type = "%s" - snapshot_retention_days = %s - daily_snapshot_retention_days = %s - point_in_time_window_hours = %s - } - backup_schedule = "%s" - } - - resource "stackit_mongodbflex_user" "user" { - project_id = stackit_mongodbflex_instance.instance.project_id - instance_id = stackit_mongodbflex_instance.instance.instance_id - username = "%s" - roles = ["%s"] - database = "%s" - } - `, - testutil.MongoDBFlexProviderConfig(), - instanceResource["project_id"], - instanceResource["name"], - instanceResource["acl"], - instanceResource["flavor_cpu"], - instanceResource["flavor_ram"], - instanceResource["replicas"], - instanceResource["storage_class"], - instanceResource["storage_size"], - version, - instanceResource["options_type"], - snapshotRetentionDays, - instanceResource["daily_snapshot_retention_days"], - instanceResource["point_in_time_window_hours"], - backupSchedule, - userResource["username"], - userResource["role"], - userResource["database"], - ) -} - -func TestAccMongoDBFlexFlexResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckMongoDBFlexDestroy, - Steps: []resource.TestStep{ - // Creation - { - Config: configResources(instanceResource["version"], instanceResource["backup_schedule"], instanceResource["snapshot_retention_days"]), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "acl.0", instanceResource["acl"]), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_instance.instance", "flavor.description"), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "flavor.cpu", instanceResource["flavor_cpu"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "flavor.ram", instanceResource["flavor_ram"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "replicas", instanceResource["replicas"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "storage.class", instanceResource["storage_class"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "storage.size", instanceResource["storage_size"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.type", instanceResource["options_type"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.snapshot_retention_days", instanceResource["snapshot_retention_days"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.daily_snapshot_retention_days", instanceResource["daily_snapshot_retention_days"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.point_in_time_window_hours", instanceResource["point_in_time_window_hours"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "backup_schedule", instanceResource["backup_schedule"]), - - // User - resource.TestCheckResourceAttrPair( - "stackit_mongodbflex_user.user", "project_id", - "stackit_mongodbflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_mongodbflex_user.user", "instance_id", - "stackit_mongodbflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_user.user", "user_id"), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_user.user", "password"), - resource.TestCheckResourceAttr("stackit_mongodbflex_user.user", "username", userResource["username"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_user.user", "database", userResource["database"]), - ), - }, - // data source - { - Config: fmt.Sprintf(` - %s - - data "stackit_mongodbflex_instance" "instance" { - project_id = stackit_mongodbflex_instance.instance.project_id - instance_id = stackit_mongodbflex_instance.instance.instance_id - } - - data "stackit_mongodbflex_user" "user" { - project_id = stackit_mongodbflex_instance.instance.project_id - instance_id = stackit_mongodbflex_instance.instance.instance_id - user_id = stackit_mongodbflex_user.user.user_id - } - `, - configResources(instanceResource["version"], instanceResource["backup_schedule"], instanceResource["snapshot_retention_days"]), - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttrPair( - "data.stackit_mongodbflex_instance.instance", "project_id", - "stackit_mongodbflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_mongodbflex_instance.instance", "instance_id", - "stackit_mongodbflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_mongodbflex_user.user", "instance_id", - "stackit_mongodbflex_user.user", "instance_id", - ), - - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "acl.0", instanceResource["acl"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "flavor.id", instanceResource["flavor_id"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "flavor.description", instanceResource["flavor_description"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "flavor.cpu", instanceResource["flavor_cpu"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "flavor.ram", instanceResource["flavor_ram"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "replicas", instanceResource["replicas"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "options.type", instanceResource["options_type"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "options.snapshot_retention_days", instanceResource["snapshot_retention_days"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "options.daily_snapshot_retention_days", instanceResource["daily_snapshot_retention_days"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "options.point_in_time_window_hours", instanceResource["point_in_time_window_hours"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_instance.instance", "backup_schedule", instanceResource["backup_schedule_read"]), - - // User data - resource.TestCheckResourceAttr("data.stackit_mongodbflex_user.user", "project_id", userResource["project_id"]), - resource.TestCheckResourceAttrSet("data.stackit_mongodbflex_user.user", "user_id"), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_user.user", "username", userResource["username"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_user.user", "database", userResource["database"]), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_user.user", "roles.#", "1"), - resource.TestCheckResourceAttr("data.stackit_mongodbflex_user.user", "roles.0", userResource["role"]), - resource.TestCheckResourceAttrSet("data.stackit_mongodbflex_user.user", "host"), - resource.TestCheckResourceAttrSet("data.stackit_mongodbflex_user.user", "port"), - ), - }, - // Import - { - ResourceName: "stackit_mongodbflex_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_mongodbflex_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_mongodbflex_instance.instance") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"backup_schedule"}, - ImportStateCheck: func(s []*terraform.InstanceState) error { - if len(s) != 1 { - return fmt.Errorf("expected 1 state, got %d", len(s)) - } - if s[0].Attributes["backup_schedule"] != instanceResource["backup_schedule_read"] { - return fmt.Errorf("expected backup_schedule %s, got %s", instanceResource["backup_schedule_read"], s[0].Attributes["backup_schedule"]) - } - return nil - }, - }, - { - ResourceName: "stackit_mongodbflex_user.user", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_mongodbflex_user.user"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_mongodbflex_user.user") - } - region, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - userId, ok := r.Primary.Attributes["user_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute user_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, region, instanceId, userId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password", "uri"}, - }, - // Update - { - Config: configResources(instanceResource["version_updated"], instanceResource["backup_schedule_updated"], instanceResource["snapshot_retention_days_updated"]), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "acl.0", instanceResource["acl"]), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttrSet("stackit_mongodbflex_instance.instance", "flavor.description"), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "flavor.cpu", instanceResource["flavor_cpu"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "flavor.ram", instanceResource["flavor_ram"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "replicas", instanceResource["replicas"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "storage.class", instanceResource["storage_class"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "storage.size", instanceResource["storage_size"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "version", instanceResource["version_updated"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.type", instanceResource["options_type"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.snapshot_retention_days", instanceResource["snapshot_retention_days_updated"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "options.point_in_time_window_hours", instanceResource["point_in_time_window_hours"]), - resource.TestCheckResourceAttr("stackit_mongodbflex_instance.instance", "backup_schedule", instanceResource["backup_schedule_updated"]), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckMongoDBFlexDestroy(s *terraform.State) error { - ctx := context.Background() - var client *mongodbflex.APIClient - var err error - if testutil.MongoDBFlexCustomEndpoint == "" { - client, err = mongodbflex.NewAPIClient() - } else { - client, err = mongodbflex.NewAPIClient( - config.WithEndpoint(testutil.MongoDBFlexCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_mongodbflex_instance" { - continue - } - // instance terraform ID: = "[project_id],[region],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[2] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId, testutil.Region).Tag("").Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - items := *instancesResp.Items - for i := range items { - if items[i].Id == nil { - continue - } - if utils.Contains(instancesToDestroy, *items[i].Id) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *items[i].Id, testutil.Region) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *items[i].Id, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *items[i].Id, testutil.Region).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *items[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/mongodbflex/user/datasource.go b/stackit/internal/services/mongodbflex/user/datasource.go deleted file mode 100644 index d9defbb1..00000000 --- a/stackit/internal/services/mongodbflex/user/datasource.go +++ /dev/null @@ -1,233 +0,0 @@ -package mongodbflex - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &userDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Database types.String `tfsdk:"database"` - Roles types.Set `tfsdk:"roles"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` -} - -// NewUserDataSource is a helper function to simplify the provider implementation. -func NewUserDataSource() datasource.DataSource { - return &userDataSource{} -} - -// userDataSource is the data source implementation. -type userDataSource struct { - client *mongodbflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (d *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mongodbflex_user" -} - -// Configure adds the provider configured client to the data source. -func (d *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mongodbflexUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "MongoDB Flex user client configured") -} - -// Schema defines the schema for the data source. -func (d *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "MongoDB Flex user data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the MongoDB Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - "roles": schema.SetAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "database": schema.StringAttribute{ - Computed: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := d.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - recordSetResp, err := d.client.GetUser(ctx, projectId, instanceId, userId, region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading user", - fmt.Sprintf("User with ID %q or instance with ID %q does not exist in project %q.", userId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema and populate Computed attribute values - err = mapDataSourceFields(recordSetResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex user read") -} - -func mapDataSourceFields(userResp *mongodbflex.GetUserResponse, model *DataSourceModel, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - model.Database = types.StringPointerValue(user.Database) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("mapping roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - return nil -} diff --git a/stackit/internal/services/mongodbflex/user/datasource_test.go b/stackit/internal/services/mongodbflex/user/datasource_test.go deleted file mode 100644 index e5ce87cf..00000000 --- a/stackit/internal/services/mongodbflex/user/datasource_test.go +++ /dev/null @@ -1,146 +0,0 @@ -package mongodbflex - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" -) - -func TestMapDataSourceFields(t *testing.T) { - tests := []struct { - description string - input *mongodbflex.GetUserResponse - region string - expected DataSourceModel - isValid bool - }{ - { - "default_values", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{}, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringNull(), - Database: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - }, - true, - }, - { - "simple_values", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{ - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Database: utils.Ptr("database"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringValue("username"), - Database: types.StringValue("database"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - }, - true, - }, - { - "null_fields_and_int_conversions", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{ - Id: utils.Ptr(userId), - Roles: &[]string{}, - Username: nil, - Database: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringNull(), - Database: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - DataSourceModel{}, - false, - }, - { - "nil_response_2", - &mongodbflex.GetUserResponse{}, - testRegion, - DataSourceModel{}, - false, - }, - { - "no_resource_id", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{}, - }, - testRegion, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &DataSourceModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapDataSourceFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/mongodbflex/user/resource.go b/stackit/internal/services/mongodbflex/user/resource.go deleted file mode 100644 index 85096f23..00000000 --- a/stackit/internal/services/mongodbflex/user/resource.go +++ /dev/null @@ -1,573 +0,0 @@ -package mongodbflex - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} - _ resource.ResourceWithImportState = &userResource{} - _ resource.ResourceWithModifyPlan = &userResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Database types.String `tfsdk:"database"` - Password types.String `tfsdk:"password"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Uri types.String `tfsdk:"uri"` - Region types.String `tfsdk:"region"` -} - -// NewUserResource is a helper function to simplify the provider implementation. -func NewUserResource() resource.Resource { - return &userResource{} -} - -// userResource is the resource implementation. -type userResource struct { - client *mongodbflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_mongodbflex_user" -} - -// Configure adds the provider configured client to the resource. -func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := mongodbflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "MongoDB Flex user client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Schema defines the schema for the resource. -func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "MongoDB Flex user resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the MongoDB Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "roles": "Database access levels for the user. Some of the possible values are: [`read`, `readWrite`, `readWriteAnyDatabase`]", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Required: true, - }, - "database": schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { - diags = model.Roles.ElementsAs(ctx, &roles, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, roles) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new user - userResp, err := r.client.CreateUser(ctx, projectId, instanceId, region).CreateUserPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user ID. A user might have been created") - return - } - userId := *userResp.Item.Id - ctx = tflog.SetField(ctx, "user_id", userId) - - // Map response body to schema - err = mapFieldsCreate(userResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex user created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - recordSetResp, err := r.client.GetUser(ctx, projectId, instanceId, userId, region).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(recordSetResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex user read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { - diags = model.Roles.ElementsAs(ctx, &roles, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, roles) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Updating API payload: %v", err)) - return - } - - // Update existing instance - err = r.client.UpdateUser(ctx, projectId, instanceId, userId, region).UpdateUserPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - userResp, err := r.client.GetUser(ctx, projectId, instanceId, userId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(userResp, &stateModel, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "MongoDB Flex user updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - // Delete user - err := r.client.DeleteUser(ctx, projectId, instanceId, userId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "MongoDB Flex user deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing user", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[3])...) - core.LogAndAddWarning(ctx, &resp.Diagnostics, - "MongoDB Flex user imported with empty password and empty uri", - "The user password and uri are not imported as they are only available upon creation of a new user. The password and uri fields will be empty.", - ) - tflog.Info(ctx, "MongoDB Flex user state imported") -} - -func mapFieldsCreate(userResp *mongodbflex.CreateUserResponse, model *Model, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - if user.Id == nil { - return fmt.Errorf("user id not present") - } - userId := *user.Id - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) - model.Region = types.StringValue(region) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - model.Database = types.StringPointerValue(user.Database) - - if user.Password == nil { - return fmt.Errorf("user password not present") - } - model.Password = types.StringValue(*user.Password) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("mapping roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - model.Uri = types.StringPointerValue(user.Uri) - return nil -} - -func mapFields(userResp *mongodbflex.GetUserResponse, model *Model, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) - model.Region = types.StringValue(region) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - model.Database = types.StringPointerValue(user.Database) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("mapping roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - return nil -} - -func toCreatePayload(model *Model, roles []string) (*mongodbflex.CreateUserPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &mongodbflex.CreateUserPayload{ - Roles: &roles, - Username: conversion.StringValueToPointer(model.Username), - Database: conversion.StringValueToPointer(model.Database), - }, nil -} - -func toUpdatePayload(model *Model, roles []string) (*mongodbflex.UpdateUserPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &mongodbflex.UpdateUserPayload{ - Roles: &roles, - Database: conversion.StringValueToPointer(model.Database), - }, nil -} diff --git a/stackit/internal/services/mongodbflex/user/resource_test.go b/stackit/internal/services/mongodbflex/user/resource_test.go deleted file mode 100644 index 53f31ef9..00000000 --- a/stackit/internal/services/mongodbflex/user/resource_test.go +++ /dev/null @@ -1,501 +0,0 @@ -package mongodbflex - -import ( - "fmt" - "testing" - - "github.com/google/uuid" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" -) - -const ( - testRegion = "eu02" -) - -var ( - projectId = uuid.NewString() - instanceId = uuid.NewString() - userId = uuid.NewString() -) - -func TestMapFieldsCreate(t *testing.T) { - tests := []struct { - description string - input *mongodbflex.CreateUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &mongodbflex.CreateUserResponse{ - Item: &mongodbflex.User{ - Id: utils.Ptr(userId), - Password: utils.Ptr(""), - }, - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringNull(), - Database: types.StringNull(), - Roles: types.SetNull(types.StringType), - Password: types.StringValue(""), - Host: types.StringNull(), - Port: types.Int64Null(), - Uri: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &mongodbflex.CreateUserResponse{ - Item: &mongodbflex.User{ - Id: utils.Ptr(userId), - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Database: utils.Ptr("database"), - Password: utils.Ptr("password"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - }, - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringValue("username"), - Database: types.StringValue("database"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Password: types.StringValue("password"), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &mongodbflex.CreateUserResponse{ - Item: &mongodbflex.User{ - Id: utils.Ptr(userId), - Roles: &[]string{}, - Username: nil, - Database: nil, - Password: utils.Ptr(""), - Host: nil, - Port: utils.Ptr(int64(2123456789)), - Uri: nil, - }, - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringNull(), - Database: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Password: types.StringValue(""), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Uri: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &mongodbflex.CreateUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &mongodbflex.CreateUserResponse{ - Item: &mongodbflex.User{}, - }, - testRegion, - Model{}, - false, - }, - { - "no_password", - &mongodbflex.CreateUserResponse{ - Item: &mongodbflex.User{ - Id: utils.Ptr(userId), - }, - }, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFieldsCreate(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *mongodbflex.GetUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{}, - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringNull(), - Database: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{ - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Database: utils.Ptr("database"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringValue("username"), - Database: types.StringValue("database"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{ - Id: utils.Ptr(userId), - Roles: &[]string{}, - Username: nil, - Database: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - }, - testRegion, - Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", projectId, testRegion, instanceId, userId)), - UserId: types.StringValue(userId), - InstanceId: types.StringValue(instanceId), - ProjectId: types.StringValue(projectId), - Username: types.StringNull(), - Database: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &mongodbflex.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &mongodbflex.GetUserResponse{ - Item: &mongodbflex.InstanceResponseUser{}, - }, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles []string - expected *mongodbflex.CreateUserPayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &mongodbflex.CreateUserPayload{ - Roles: &[]string{}, - Username: nil, - Database: nil, - }, - true, - }, - { - "default_values", - &Model{ - Username: types.StringValue("username"), - Database: types.StringValue("database"), - }, - []string{ - "role_1", - "role_2", - }, - &mongodbflex.CreateUserPayload{ - Roles: &[]string{ - "role_1", - "role_2", - }, - Username: utils.Ptr("username"), - Database: utils.Ptr("database"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - Database: types.StringNull(), - }, - []string{ - "", - }, - &mongodbflex.CreateUserPayload{ - Roles: &[]string{ - "", - }, - Username: nil, - Database: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputRoles) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles []string - expected *mongodbflex.UpdateUserPayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &mongodbflex.UpdateUserPayload{ - Roles: &[]string{}, - Database: nil, - }, - true, - }, - { - "simple values", - &Model{ - Username: types.StringValue("username"), - Database: types.StringValue("database"), - }, - []string{ - "role_1", - "role_2", - }, - &mongodbflex.UpdateUserPayload{ - Roles: &[]string{ - "role_1", - "role_2", - }, - Database: utils.Ptr("database"), - }, - true, - }, - { - "null_fields", - &Model{ - Username: types.StringNull(), - Database: types.StringNull(), - }, - []string{ - "", - }, - &mongodbflex.UpdateUserPayload{ - Roles: &[]string{ - "", - }, - Database: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputRoles) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/mongodbflex/utils/util.go b/stackit/internal/services/mongodbflex/utils/util.go deleted file mode 100644 index e1c805c1..00000000 --- a/stackit/internal/services/mongodbflex/utils/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *mongodbflex.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.MongoDBFlexCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.MongoDBFlexCustomEndpoint)) - } - - apiClient, err := mongodbflex.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/mongodbflex/utils/util_test.go b/stackit/internal/services/mongodbflex/utils/util_test.go deleted file mode 100644 index d268d9af..00000000 --- a/stackit/internal/services/mongodbflex/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://mongodbflex-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *mongodbflex.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *mongodbflex.APIClient { - apiClient, err := mongodbflex.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - MongoDBFlexCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *mongodbflex.APIClient { - apiClient, err := mongodbflex.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/objectstorage/bucket/datasource.go b/stackit/internal/services/objectstorage/bucket/datasource.go deleted file mode 100644 index 52a981b0..00000000 --- a/stackit/internal/services/objectstorage/bucket/datasource.go +++ /dev/null @@ -1,159 +0,0 @@ -package objectstorage - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &bucketDataSource{} -) - -// NewBucketDataSource is a helper function to simplify the provider implementation. -func NewBucketDataSource() datasource.DataSource { - return &bucketDataSource{} -} - -// bucketDataSource is the data source implementation. -type bucketDataSource struct { - client *objectstorage.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *bucketDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_objectstorage_bucket" -} - -// Configure adds the provider configured client to the data source. -func (r *bucketDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "ObjectStorage bucket client configured") -} - -// Schema defines the schema for the data source. -func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "ObjectStorage bucket data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`name`\".", - "name": "The bucket name. It must be DNS conform.", - "project_id": "STACKIT Project ID to which the bucket is associated.", - "url_path_style": "URL in path style.", - "url_virtual_hosted_style": "URL in virtual hosted style.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "url_path_style": schema.StringAttribute{ - Computed: true, - }, - "url_virtual_hosted_style": schema.StringAttribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found automatically, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - bucketName := model.Name.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", bucketName) - ctx = tflog.SetField(ctx, "region", region) - - bucketResp, err := r.client.GetBucket(ctx, projectId, region, bucketName).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading bucket", - fmt.Sprintf("Bucket with name %q does not exist in project %q.", bucketName, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(bucketResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage bucket read") -} diff --git a/stackit/internal/services/objectstorage/bucket/resource.go b/stackit/internal/services/objectstorage/bucket/resource.go deleted file mode 100644 index f1b8f371..00000000 --- a/stackit/internal/services/objectstorage/bucket/resource.go +++ /dev/null @@ -1,375 +0,0 @@ -package objectstorage - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &bucketResource{} - _ resource.ResourceWithConfigure = &bucketResource{} - _ resource.ResourceWithImportState = &bucketResource{} - _ resource.ResourceWithModifyPlan = &bucketResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - Name types.String `tfsdk:"name"` - ProjectId types.String `tfsdk:"project_id"` - URLPathStyle types.String `tfsdk:"url_path_style"` - URLVirtualHostedStyle types.String `tfsdk:"url_virtual_hosted_style"` - Region types.String `tfsdk:"region"` -} - -// NewBucketResource is a helper function to simplify the provider implementation. -func NewBucketResource() resource.Resource { - return &bucketResource{} -} - -// bucketResource is the resource implementation. -type bucketResource struct { - client *objectstorage.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *bucketResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_objectstorage_bucket" -} - -// Configure adds the provider configured client to the resource. -func (r *bucketResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "ObjectStorage bucket client configured") -} - -// Schema defines the schema for the resource. -func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "ObjectStorage bucket resource schema. Must have a `region` specified in the provider configuration. If you are creating `credentialsgroup` and `bucket` resources simultaneously, please include the `depends_on` field so that they are created sequentially. This prevents errors from concurrent calls to the service enablement that is done in the background.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`name`\".", - "name": "The bucket name. It must be DNS conform.", - "project_id": "STACKIT Project ID to which the bucket is associated.", - "url_path_style": "URL in path style.", - "url_virtual_hosted_style": "URL in virtual hosted style.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "url_path_style": schema.StringAttribute{ - Computed: true, - }, - "url_virtual_hosted_style": schema.StringAttribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - bucketName := model.Name.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", bucketName) - ctx = tflog.SetField(ctx, "region", region) - - // Handle project init - err := enableProject(ctx, &model, region, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Enabling object storage project before creation: %v", err)) - return - } - - // Create new bucket - _, err = r.client.CreateBucket(ctx, projectId, region, bucketName).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.CreateBucketWaitHandler(ctx, r.client, projectId, region, bucketName).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Bucket creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage bucket created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - bucketName := model.Name.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", bucketName) - ctx = tflog.SetField(ctx, "region", region) - - bucketResp, err := r.client.GetBucket(ctx, projectId, region, bucketName).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(bucketResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage bucket read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *bucketResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating bucket", "Bucket can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - bucketName := model.Name.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", bucketName) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing bucket - _, err := r.client.DeleteBucket(ctx, projectId, region, bucketName).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - if errors.As(err, &oapiErr) { - if oapiErr.StatusCode == http.StatusUnprocessableEntity { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", "Bucket isn't empty and cannot be deleted") - return - } - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteBucketWaitHandler(ctx, r.client, projectId, region, bucketName).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Bucket deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "ObjectStorage bucket deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name -func (r *bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing bucket", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[name], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "ObjectStorage bucket state imported") -} - -func mapFields(bucketResp *objectstorage.GetBucketResponse, model *Model, region string) error { - if bucketResp == nil { - return fmt.Errorf("response input is nil") - } - if bucketResp.Bucket == nil { - return fmt.Errorf("response bucket is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - bucket := bucketResp.Bucket - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.Name.ValueString()) - model.URLPathStyle = types.StringPointerValue(bucket.UrlPathStyle) - model.URLVirtualHostedStyle = types.StringPointerValue(bucket.UrlVirtualHostedStyle) - model.Region = types.StringValue(region) - return nil -} - -type objectStorageClient interface { - EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error) -} - -// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens -func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error { - projectId := model.ProjectId.ValueString() - - // From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate - _, err := client.EnableServiceExecute(ctx, projectId, region) - if err != nil { - return fmt.Errorf("failed to create object storage project: %w", err) - } - return nil -} diff --git a/stackit/internal/services/objectstorage/bucket/resource_test.go b/stackit/internal/services/objectstorage/bucket/resource_test.go deleted file mode 100644 index 876e2fd5..00000000 --- a/stackit/internal/services/objectstorage/bucket/resource_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package objectstorage - -import ( - "context" - _ "embed" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -type objectStorageClientMocked struct { - returnError bool -} - -func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId, _ string) (*objectstorage.ProjectStatus, error) { - if c.returnError { - return nil, fmt.Errorf("create project failed") - } - - return &objectstorage.ProjectStatus{ - Project: utils.Ptr(projectId), - }, nil -} - -func TestMapFields(t *testing.T) { - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "bname") - tests := []struct { - description string - input *objectstorage.GetBucketResponse - expected Model - isValid bool - }{ - { - "default_values", - &objectstorage.GetBucketResponse{ - Bucket: &objectstorage.Bucket{}, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringValue("bname"), - ProjectId: types.StringValue("pid"), - URLPathStyle: types.StringNull(), - URLVirtualHostedStyle: types.StringNull(), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "simple_values", - &objectstorage.GetBucketResponse{ - Bucket: &objectstorage.Bucket{ - UrlPathStyle: utils.Ptr("url/path/style"), - UrlVirtualHostedStyle: utils.Ptr("url/virtual/hosted/style"), - }, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringValue("bname"), - ProjectId: types.StringValue("pid"), - URLPathStyle: types.StringValue("url/path/style"), - URLVirtualHostedStyle: types.StringValue("url/virtual/hosted/style"), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "empty_strings", - &objectstorage.GetBucketResponse{ - Bucket: &objectstorage.Bucket{ - UrlPathStyle: utils.Ptr(""), - UrlVirtualHostedStyle: utils.Ptr(""), - }, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringValue("bname"), - ProjectId: types.StringValue("pid"), - URLPathStyle: types.StringValue(""), - URLVirtualHostedStyle: types.StringValue(""), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_bucket", - &objectstorage.GetBucketResponse{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - Name: tt.expected.Name, - } - err := mapFields(tt.input, model, "eu01") - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestEnableProject(t *testing.T) { - tests := []struct { - description string - enableFails bool - isValid bool - }{ - { - "default_values", - false, - true, - }, - { - "error_response", - true, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &objectStorageClientMocked{ - returnError: tt.enableFails, - } - err := enableProject(context.Background(), &Model{}, "eu01", client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - }) - } -} diff --git a/stackit/internal/services/objectstorage/credential/datasource.go b/stackit/internal/services/objectstorage/credential/datasource.go deleted file mode 100644 index 27841de8..00000000 --- a/stackit/internal/services/objectstorage/credential/datasource.go +++ /dev/null @@ -1,226 +0,0 @@ -package objectstorage - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &credentialDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialId types.String `tfsdk:"credential_id"` - CredentialsGroupId types.String `tfsdk:"credentials_group_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - ExpirationTimestamp types.String `tfsdk:"expiration_timestamp"` - Region types.String `tfsdk:"region"` -} - -// NewCredentialDataSource is a helper function to simplify the provider implementation. -func NewCredentialDataSource() datasource.DataSource { - return &credentialDataSource{} -} - -// credentialDataSource is the resource implementation. -type credentialDataSource struct { - client *objectstorage.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_objectstorage_credential" -} - -// Configure adds the provider configured client to the datasource. -func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "ObjectStorage credential client configured") -} - -// Schema defines the schema for the datasource. -func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "ObjectStorage credential data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`credentials_group_id`,`credential_id`\".", - "credential_id": "The credential ID.", - "credentials_group_id": "The credential group ID.", - "project_id": "STACKIT Project ID to which the credential group is associated.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Required: true, - }, - "credentials_group_id": schema.StringAttribute{ - Description: descriptions["credentials_group_id"], - Required: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - }, - "name": schema.StringAttribute{ - Computed: true, - }, - "expiration_timestamp": schema.StringAttribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found automatically, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - credentialId := model.CredentialId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - ctx = tflog.SetField(ctx, "region", region) - - credentialsGroupResp, err := r.client.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading credential", - fmt.Sprintf("Credential group with ID %q does not exist in project %q.", credentialsGroupId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - if credentialsGroupResp == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Reading credentials", fmt.Sprintf("Response is nil: %v", err)) - return - } - - credential := findCredential(*credentialsGroupResp, credentialId) - if credential == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Reading credential", fmt.Sprintf("Credential with ID %q not found in credentials group %q", credentialId, credentialsGroupId)) - return - } - - err = mapDataSourceFields(credential, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage credential read") -} - -func mapDataSourceFields(credentialResp *objectstorage.AccessKey, model *DataSourceModel, region string) error { - if credentialResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var credentialId string - if model.CredentialId.ValueString() != "" { - credentialId = model.CredentialId.ValueString() - } else if credentialResp.KeyId != nil { - credentialId = *credentialResp.KeyId - } else { - return fmt.Errorf("credential id not present") - } - - if credentialResp.Expires == nil { - model.ExpirationTimestamp = types.StringNull() - } else { - // Harmonize the timestamp format - // Eg. "2027-01-02T03:04:05.000Z" = "2027-01-02T03:04:05Z" - expirationTimestamp, err := time.Parse(time.RFC3339, *credentialResp.Expires) - if err != nil { - return fmt.Errorf("unable to parse payload expiration timestamp '%v': %w", *credentialResp.Expires, err) - } - model.ExpirationTimestamp = types.StringValue(expirationTimestamp.Format(time.RFC3339)) - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.CredentialsGroupId.ValueString(), credentialId, - ) - model.CredentialId = types.StringValue(credentialId) - model.Name = types.StringPointerValue(credentialResp.DisplayName) - model.Region = types.StringValue(region) - return nil -} - -// Returns the access key if found otherwise nil -func findCredential(credentialsGroupResp objectstorage.ListAccessKeysResponse, credentialId string) *objectstorage.AccessKey { - for _, credential := range *credentialsGroupResp.AccessKeys { - if credential.KeyId == nil || *credential.KeyId != credentialId { - continue - } - return &credential - } - return nil -} diff --git a/stackit/internal/services/objectstorage/credential/datasource_test.go b/stackit/internal/services/objectstorage/credential/datasource_test.go deleted file mode 100644 index e6ba0539..00000000 --- a/stackit/internal/services/objectstorage/credential/datasource_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package objectstorage - -import ( - "fmt" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -func TestMapDatasourceFields(t *testing.T) { - now := time.Now() - - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") - tests := []struct { - description string - input *objectstorage.AccessKey - expected DataSourceModel - isValid bool - }{ - { - "default_values", - &objectstorage.AccessKey{}, - DataSourceModel{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringNull(), - ExpirationTimestamp: types.StringNull(), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "simple_values", - &objectstorage.AccessKey{ - DisplayName: utils.Ptr("name"), - Expires: utils.Ptr(now.Format(time.RFC3339)), - }, - DataSourceModel{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringValue("name"), - ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "empty_strings", - &objectstorage.AccessKey{ - DisplayName: utils.Ptr(""), - }, - DataSourceModel{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringValue(""), - ExpirationTimestamp: types.StringNull(), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "expiration_timestamp_with_fractional_seconds", - &objectstorage.AccessKey{ - Expires: utils.Ptr(now.Format(time.RFC3339Nano)), - }, - DataSourceModel{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringNull(), - ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "nil_response", - nil, - DataSourceModel{}, - false, - }, - { - "bad_time", - &objectstorage.AccessKey{ - Expires: utils.Ptr("foo-bar"), - }, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &DataSourceModel{ - ProjectId: tt.expected.ProjectId, - CredentialsGroupId: tt.expected.CredentialsGroupId, - CredentialId: tt.expected.CredentialId, - } - err := mapDataSourceFields(tt.input, model, "eu01") - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/objectstorage/credential/resource.go b/stackit/internal/services/objectstorage/credential/resource.go deleted file mode 100644 index a2125a1e..00000000 --- a/stackit/internal/services/objectstorage/credential/resource.go +++ /dev/null @@ -1,580 +0,0 @@ -package objectstorage - -import ( - "context" - "fmt" - "net/http" - "strings" - "time" - - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialResource{} - _ resource.ResourceWithConfigure = &credentialResource{} - _ resource.ResourceWithImportState = &credentialResource{} - _ resource.ResourceWithModifyPlan = &credentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialId types.String `tfsdk:"credential_id"` - CredentialsGroupId types.String `tfsdk:"credentials_group_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - AccessKey types.String `tfsdk:"access_key"` - SecretAccessKey types.String `tfsdk:"secret_access_key"` - ExpirationTimestamp types.String `tfsdk:"expiration_timestamp"` - Region types.String `tfsdk:"region"` -} - -// NewCredentialResource is a helper function to simplify the provider implementation. -func NewCredentialResource() resource.Resource { - return &credentialResource{} -} - -// credentialResource is the resource implementation. -type credentialResource struct { - client *objectstorage.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -func (r *credentialResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - r.modifyPlanRegion(ctx, &req, resp) - if resp.Diagnostics.HasError() { - return - } - r.modifyPlanExpiration(ctx, &req, resp) - if resp.Diagnostics.HasError() { - return - } -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *credentialResource) modifyPlanRegion(ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -func (r *credentialResource) modifyPlanExpiration(ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - p := path.Root("expiration_timestamp") - var ( - stateDate time.Time - planDate time.Time - ) - - resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, p, req.State, time.RFC3339, &stateDate)...) - if resp.Diagnostics.HasError() { - return - } - resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, p, resp.Plan, time.RFC3339, &planDate)...) - if resp.Diagnostics.HasError() { - return - } - - // replace the planned expiration time with the current state date, iff they represent - // the same point in time (but perhaps with different textual representation) - // this will prevent no-op updates - if stateDate.Equal(planDate) && !stateDate.IsZero() { - resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, p, types.StringValue(stateDate.Format(time.RFC3339)))...) - if resp.Diagnostics.HasError() { - return - } - } -} - -// Metadata returns the resource type name. -func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_objectstorage_credential" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "ObjectStorage credential client configured") -} - -// Schema defines the schema for the resource. -func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "ObjectStorage credential resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`credentials_group_id`,`credential_id`\".", - "credential_id": "The credential ID.", - "credentials_group_id": "The credential group ID.", - "project_id": "STACKIT Project ID to which the credential group is associated.", - "expiration_timestamp": "Expiration timestamp, in RFC339 format without fractional seconds. Example: \"2025-01-01T00:00:00Z\". If not set, the credential never expires.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "credentials_group_id": schema.StringAttribute{ - Description: descriptions["credentials_group_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Computed: true, - }, - "access_key": schema.StringAttribute{ - Computed: true, - }, - "secret_access_key": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "expiration_timestamp": schema.StringAttribute{ - Description: descriptions["expiration_timestamp"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.RFC3339SecondsOnly(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId) - ctx = tflog.SetField(ctx, "region", region) - - // Handle project init - err := enableProject(ctx, &model, region, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Enabling object storage project before creation: %v", err)) - return - } - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new credential - credentialResp, err := r.client.CreateAccessKey(ctx, projectId, region).CredentialsGroup(credentialsGroupId).CreateAccessKeyPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if credentialResp.KeyId == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") - return - } - credentialId := *credentialResp.KeyId - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - // Map response body to schema - err = mapFields(credentialResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - if !utils.IsUndefined(model.ExpirationTimestamp) { - var ( - actualDate time.Time - planDate time.Time - ) - resp.Diagnostics.Append(utils.ToTime(ctx, time.RFC3339, model.ExpirationTimestamp, &actualDate)...) - if resp.Diagnostics.HasError() { - return - } - resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, path.Root("expiration_timestamp"), req.Plan, time.RFC3339, &planDate)...) - if resp.Diagnostics.HasError() { - return - } - // replace the planned expiration date with the original date, iff - // they represent the same point in time, (perhaps with different textual representations) - if actualDate.Equal(planDate) { - model.ExpirationTimestamp = types.StringValue(planDate.Format(time.RFC3339)) - } - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage credential created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - credentialId := model.CredentialId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - ctx = tflog.SetField(ctx, "region", region) - - found, err := readCredentials(ctx, &model, region, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if !found { - resp.State.RemoveResource(ctx) - return - } - var ( - currentApiDate time.Time - stateDate time.Time - ) - - if !utils.IsUndefined(model.ExpirationTimestamp) { - resp.Diagnostics.Append(utils.ToTime(ctx, time.RFC3339, model.ExpirationTimestamp, ¤tApiDate)...) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, path.Root("expiration_timestamp"), req.State, time.RFC3339, &stateDate)...) - if resp.Diagnostics.HasError() { - return - } - - // replace the resulting expiration date with the original date, iff - // they represent the same point in time, (perhaps with different textual representations) - if currentApiDate.Equal(stateDate) { - model.ExpirationTimestamp = types.StringValue(stateDate.Format(time.RFC3339)) - } - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage credential read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - /* - While a credential cannot be updated, the Update call must not be prevented with an error: - When the expiration timestamp has been updated to the same point in time, but e.g. with a different timezone, - terraform will still trigger an Update due to the computed attributes. These will not change, - but terraform has no way of knowing this without calling this function. So it is - still updated as a no-op. - A possible enhancement would be to emit an error, if it is attempted to change one of the not computed attributes - and abort with an error in this case. - */ -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - credentialId := model.CredentialId.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing credential - _, err := r.client.DeleteAccessKey(ctx, projectId, region, credentialId).CredentialsGroup(credentialsGroupId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "ObjectStorage credential deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,credentials_group_id,credential_id -func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[credentials_group_id],[credential_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credentials_group_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[3])...) - tflog.Info(ctx, "ObjectStorage credential state imported") -} - -type objectStorageClient interface { - EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error) -} - -// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens -func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error { - projectId := model.ProjectId.ValueString() - - // From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate - _, err := client.EnableServiceExecute(ctx, projectId, region) - if err != nil { - return fmt.Errorf("failed to create object storage project: %w", err) - } - return nil -} - -func toCreatePayload(model *Model) (*objectstorage.CreateAccessKeyPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - if model.ExpirationTimestamp.IsNull() || model.ExpirationTimestamp.IsUnknown() { - return &objectstorage.CreateAccessKeyPayload{}, nil - } - - expirationTimestampValue := conversion.StringValueToPointer(model.ExpirationTimestamp) - if expirationTimestampValue == nil { - return &objectstorage.CreateAccessKeyPayload{}, nil - } - expirationTimestamp, err := time.Parse(time.RFC3339, *expirationTimestampValue) - if err != nil { - return nil, fmt.Errorf("unable to parse expiration timestamp '%v': %w", *expirationTimestampValue, err) - } - return &objectstorage.CreateAccessKeyPayload{ - Expires: &expirationTimestamp, - }, nil -} - -func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Model, region string) error { - if credentialResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var credentialId string - if model.CredentialId.ValueString() != "" { - credentialId = model.CredentialId.ValueString() - } else if credentialResp.KeyId != nil { - credentialId = *credentialResp.KeyId - } else { - return fmt.Errorf("credential id not present") - } - - if credentialResp.Expires == nil { - model.ExpirationTimestamp = types.StringNull() - } else { - // Harmonize the timestamp format - // Eg. "2027-01-02T03:04:05.000Z" = "2027-01-02T03:04:05Z" - expirationTimestamp, err := time.Parse(time.RFC3339, *credentialResp.Expires.Get()) - if err != nil { - return fmt.Errorf("unable to parse payload expiration timestamp '%v': %w", *credentialResp.Expires, err) - } - model.ExpirationTimestamp = types.StringValue(expirationTimestamp.Format(time.RFC3339)) - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.CredentialsGroupId.ValueString(), credentialId, - ) - model.CredentialId = types.StringValue(credentialId) - model.Name = types.StringPointerValue(credentialResp.DisplayName) - model.AccessKey = types.StringPointerValue(credentialResp.AccessKey) - model.SecretAccessKey = types.StringPointerValue(credentialResp.SecretAccessKey) - model.Region = types.StringValue(region) - return nil -} - -// readCredentials gets all the existing credentials for the specified credentials group, -// finds the credential that is being read and updates the state. -// Returns True if the credential was found, False otherwise. -func readCredentials(ctx context.Context, model *Model, region string, client *objectstorage.APIClient) (bool, error) { - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - credentialId := model.CredentialId.ValueString() - - credentialsGroupResp, err := client.ListAccessKeys(ctx, projectId, region).CredentialsGroup(credentialsGroupId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - return false, nil - } - return false, fmt.Errorf("getting credentials groups: %w", err) - } - if credentialsGroupResp == nil { - return false, fmt.Errorf("getting credentials groups: nil response") - } - - foundCredential := false - for _, credential := range *credentialsGroupResp.AccessKeys { - if credential.KeyId == nil || *credential.KeyId != credentialId { - continue - } - - foundCredential = true - - model.Id = utils.BuildInternalTerraformId(projectId, region, credentialsGroupId, credentialId) - model.Name = types.StringPointerValue(credential.DisplayName) - - if credential.Expires == nil { - model.ExpirationTimestamp = types.StringNull() - } else { - // Harmonize the timestamp format - // Eg. "2027-01-02T03:04:05.000Z" = "2027-01-02T03:04:05Z" - expirationTimestamp, err := time.Parse(time.RFC3339, *credential.Expires) - if err != nil { - return foundCredential, fmt.Errorf("unable to parse payload expiration timestamp '%v': %w", *credential.Expires, err) - } - model.ExpirationTimestamp = types.StringValue(expirationTimestamp.Format(time.RFC3339)) - } - break - } - model.Region = types.StringValue(region) - - return foundCredential, nil -} diff --git a/stackit/internal/services/objectstorage/credential/resource_test.go b/stackit/internal/services/objectstorage/credential/resource_test.go deleted file mode 100644 index 24746aa2..00000000 --- a/stackit/internal/services/objectstorage/credential/resource_test.go +++ /dev/null @@ -1,450 +0,0 @@ -package objectstorage - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -type objectStorageClientMocked struct { - returnError bool -} - -func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId, _ string) (*objectstorage.ProjectStatus, error) { - if c.returnError { - return nil, fmt.Errorf("create project failed") - } - - return &objectstorage.ProjectStatus{ - Project: utils.Ptr(projectId), - }, nil -} - -func TestMapFields(t *testing.T) { - now := time.Now() - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") - tests := []struct { - description string - input *objectstorage.CreateAccessKeyResponse - expected Model - isValid bool - }{ - { - "default_values", - &objectstorage.CreateAccessKeyResponse{}, - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringNull(), - AccessKey: types.StringNull(), - SecretAccessKey: types.StringNull(), - ExpirationTimestamp: types.StringNull(), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "simple_values", - &objectstorage.CreateAccessKeyResponse{ - AccessKey: utils.Ptr("key"), - DisplayName: utils.Ptr("name"), - Expires: objectstorage.NewNullableString(utils.Ptr(now.Format(time.RFC3339))), - SecretAccessKey: utils.Ptr("secret-key"), - }, - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringValue("name"), - AccessKey: types.StringValue("key"), - SecretAccessKey: types.StringValue("secret-key"), - ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "empty_strings", - &objectstorage.CreateAccessKeyResponse{ - AccessKey: utils.Ptr(""), - DisplayName: utils.Ptr(""), - SecretAccessKey: utils.Ptr(""), - }, - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringValue(""), - AccessKey: types.StringValue(""), - SecretAccessKey: types.StringValue(""), - ExpirationTimestamp: types.StringNull(), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "expiration_timestamp_with_fractional_seconds", - &objectstorage.CreateAccessKeyResponse{ - Expires: objectstorage.NewNullableString(utils.Ptr(now.Format(time.RFC3339Nano))), - }, - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringNull(), - AccessKey: types.StringNull(), - ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "bad_time", - &objectstorage.CreateAccessKeyResponse{ - Expires: objectstorage.NewNullableString(utils.Ptr("foo-bar")), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - CredentialsGroupId: tt.expected.CredentialsGroupId, - CredentialId: tt.expected.CredentialId, - } - err := mapFields(tt.input, model, "eu01") - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestEnableProject(t *testing.T) { - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") - tests := []struct { - description string - expected Model - enableFails bool - isValid bool - }{ - { - "default_values", - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringNull(), - AccessKey: types.StringNull(), - SecretAccessKey: types.StringNull(), - ExpirationTimestamp: types.StringNull(), - }, - false, - true, - }, - { - "error_response", - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringNull(), - AccessKey: types.StringNull(), - SecretAccessKey: types.StringNull(), - ExpirationTimestamp: types.StringNull(), - }, - true, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &objectStorageClientMocked{ - returnError: tt.enableFails, - } - model := &Model{ - ProjectId: tt.expected.ProjectId, - CredentialsGroupId: tt.expected.CredentialsGroupId, - CredentialId: tt.expected.CredentialId, - } - err := enableProject(context.Background(), model, "eu01", client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - }) - } -} - -func TestReadCredentials(t *testing.T) { - now := time.Now() - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") - tests := []struct { - description string - mockedResp *objectstorage.ListAccessKeysResponse - expectedModel Model - expectedFound bool - getCredentialsFails bool - isValid bool - }{ - { - "default_values", - &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ - { - KeyId: utils.Ptr("foo-cid"), - }, - { - KeyId: utils.Ptr("bar-cid"), - }, - { - KeyId: utils.Ptr("cid"), - }, - }, - }, - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringNull(), - AccessKey: types.StringNull(), - SecretAccessKey: types.StringNull(), - ExpirationTimestamp: types.StringNull(), - Region: types.StringValue("eu01"), - }, - true, - false, - true, - }, - { - "simple_values", - &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ - { - KeyId: utils.Ptr("foo-cid"), - DisplayName: utils.Ptr("foo-name"), - Expires: utils.Ptr(now.Add(time.Hour).Format(time.RFC3339)), - }, - { - KeyId: utils.Ptr("bar-cid"), - DisplayName: utils.Ptr("bar-name"), - Expires: utils.Ptr(now.Add(time.Minute).Format(time.RFC3339)), - }, - { - KeyId: utils.Ptr("cid"), - DisplayName: utils.Ptr("name"), - Expires: utils.Ptr(now.Format(time.RFC3339)), - }, - }, - }, - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringValue("name"), - AccessKey: types.StringNull(), - SecretAccessKey: types.StringNull(), - ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), - Region: types.StringValue("eu01"), - }, - true, - false, - true, - }, - { - "expiration_timestamp_with_fractional_seconds", - &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ - { - KeyId: utils.Ptr("foo-cid"), - DisplayName: utils.Ptr("foo-name"), - Expires: utils.Ptr(now.Add(time.Hour).Format(time.RFC3339Nano)), - }, - { - KeyId: utils.Ptr("bar-cid"), - DisplayName: utils.Ptr("bar-name"), - Expires: utils.Ptr(now.Add(time.Minute).Format(time.RFC3339Nano)), - }, - { - KeyId: utils.Ptr("cid"), - DisplayName: utils.Ptr("name"), - Expires: utils.Ptr(now.Format(time.RFC3339Nano)), - }, - }, - }, - Model{ - Id: types.StringValue(id), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cgid"), - CredentialId: types.StringValue("cid"), - Name: types.StringValue("name"), - AccessKey: types.StringNull(), - SecretAccessKey: types.StringNull(), - ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), - Region: types.StringValue("eu01"), - }, - true, - false, - true, - }, - { - "empty_credentials", - &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{}, - }, - Model{ - Region: types.StringValue("eu01"), - }, - false, - false, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - false, - false, - }, - { - "non_matching_credential", - &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ - { - KeyId: utils.Ptr("foo-cid"), - DisplayName: utils.Ptr("foo-name"), - Expires: utils.Ptr(now.Add(time.Hour).Format(time.RFC3339)), - }, - { - KeyId: utils.Ptr("bar-cid"), - DisplayName: utils.Ptr("bar-name"), - Expires: utils.Ptr(now.Add(time.Minute).Format(time.RFC3339)), - }, - }, - }, - Model{ - Region: types.StringValue("eu01"), - }, - false, - false, - true, - }, - { - "error_response", - &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ - { - KeyId: utils.Ptr("cid"), - DisplayName: utils.Ptr("name"), - Expires: utils.Ptr(now.Format(time.RFC3339)), - }, - }, - }, - Model{}, - false, - true, - false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - mockedRespBytes, err := json.Marshal(tt.mockedResp) - if err != nil { - t.Fatalf("Failed to marshal mocked response: %v", err) - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - if tt.getCredentialsFails { - w.WriteHeader(http.StatusBadGateway) - w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte("{\"message\": \"Something bad happened\"")) - if err != nil { - t.Errorf("Failed to write bad response: %v", err) - } - return - } - - _, err := w.Write(mockedRespBytes) - if err != nil { - t.Errorf("Failed to write response: %v", err) - } - }) - mockedServer := httptest.NewServer(handler) - defer mockedServer.Close() - client, err := objectstorage.NewAPIClient( - config.WithEndpoint(mockedServer.URL), - config.WithoutAuthentication(), - ) - if err != nil { - t.Fatalf("Failed to initialize client: %v", err) - } - - model := &Model{ - ProjectId: tt.expectedModel.ProjectId, - CredentialsGroupId: tt.expectedModel.CredentialsGroupId, - CredentialId: tt.expectedModel.CredentialId, - } - found, err := readCredentials(context.Background(), model, "eu01", client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - - if found != tt.expectedFound { - t.Fatalf("Found does not match: %v", found) - } - } - }) - } -} diff --git a/stackit/internal/services/objectstorage/credentialsgroup/datasource.go b/stackit/internal/services/objectstorage/credentialsgroup/datasource.go deleted file mode 100644 index de690b5d..00000000 --- a/stackit/internal/services/objectstorage/credentialsgroup/datasource.go +++ /dev/null @@ -1,150 +0,0 @@ -package objectstorage - -import ( - "context" - "fmt" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &credentialsGroupDataSource{} -) - -// NewCredentialsGroupDataSource is a helper function to simplify the provider implementation. -func NewCredentialsGroupDataSource() datasource.DataSource { - return &credentialsGroupDataSource{} -} - -// credentialsGroupDataSource is the data source implementation. -type credentialsGroupDataSource struct { - client *objectstorage.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *credentialsGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_objectstorage_credentials_group" -} - -// Configure adds the provider configured client to the data source. -func (r *credentialsGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "ObjectStorage credentials group client configured") -} - -// Schema defines the schema for the data source. -func (r *credentialsGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "ObjectStorage credentials group data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`credentials_group_id`\".", - "credentials_group_id": "The credentials group ID.", - "name": "The credentials group's display name.", - "project_id": "Object Storage Project ID to which the credentials group is associated.", - "urn": "Credentials group uniform resource name (URN)", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "credentials_group_id": schema.StringAttribute{ - Description: descriptions["credentials_group_id"], - Required: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "urn": schema.StringAttribute{ - Computed: true, - Description: descriptions["urn"], - }, - "region": schema.StringAttribute{ - // the region cannot be found automatically, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId) - ctx = tflog.SetField(ctx, "region", region) - - found, err := readCredentialsGroups(ctx, &model, region, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials group", fmt.Sprintf("getting credential group from list of credentials groups: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if !found { - resp.State.RemoveResource(ctx) - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials group", fmt.Sprintf("Credentials group with ID %q does not exists in project %q", credentialsGroupId, projectId)) - return - } - - // update the region attribute manually, as it is not contained in the - // server response - model.Region = types.StringValue(region) - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage credentials group read") -} diff --git a/stackit/internal/services/objectstorage/credentialsgroup/resource.go b/stackit/internal/services/objectstorage/credentialsgroup/resource.go deleted file mode 100644 index 9973ba80..00000000 --- a/stackit/internal/services/objectstorage/credentialsgroup/resource.go +++ /dev/null @@ -1,411 +0,0 @@ -package objectstorage - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialsGroupResource{} - _ resource.ResourceWithConfigure = &credentialsGroupResource{} - _ resource.ResourceWithImportState = &credentialsGroupResource{} - _ resource.ResourceWithModifyPlan = &credentialsGroupResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialsGroupId types.String `tfsdk:"credentials_group_id"` - Name types.String `tfsdk:"name"` - ProjectId types.String `tfsdk:"project_id"` - URN types.String `tfsdk:"urn"` - Region types.String `tfsdk:"region"` -} - -// NewCredentialsGroupResource is a helper function to simplify the provider implementation. -func NewCredentialsGroupResource() resource.Resource { - return &credentialsGroupResource{} -} - -// credentialsGroupResource is the resource implementation. -type credentialsGroupResource struct { - client *objectstorage.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *credentialsGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *credentialsGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_objectstorage_credentials_group" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialsGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "ObjectStorage credentials group client configured") -} - -// Schema defines the schema for the resource. -func (r *credentialsGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "ObjectStorage credentials group resource schema. Must have a `region` specified in the provider configuration. If you are creating `credentialsgroup` and `bucket` resources simultaneously, please include the `depends_on` field so that they are created sequentially. This prevents errors from concurrent calls to the service enablement that is done in the background.", - "id": "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`credentials_group_id`\".", - "credentials_group_id": "The credentials group ID", - "name": "The credentials group's display name.", - "project_id": "Project ID to which the credentials group is associated.", - "urn": "Credentials group uniform resource name (URN)", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credentials_group_id": schema.StringAttribute{ - Description: descriptions["credentials_group_id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "urn": schema.StringAttribute{ - Description: descriptions["urn"], - Computed: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialsGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupName := model.Name.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", credentialsGroupName) - ctx = tflog.SetField(ctx, "region", region) - - createCredentialsGroupPayload := objectstorage.CreateCredentialsGroupPayload{ - DisplayName: sdkUtils.Ptr(credentialsGroupName), - } - - // Handle project init - err := enableProject(ctx, &model, region, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Enabling object storage project before creation: %v", err)) - return - } - - // Create new credentials group - got, err := r.client.CreateCredentialsGroup(ctx, projectId, region).CreateCredentialsGroupPayload(createCredentialsGroupPayload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentials group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(got, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentialsGroup", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage credentials group created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId) - ctx = tflog.SetField(ctx, "region", region) - - found, err := readCredentialsGroups(ctx, &model, region, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentialsGroup", fmt.Sprintf("getting credential group from list of credentials groups: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if !found { - resp.State.RemoveResource(ctx) - return - } - // update the region manually - model.Region = types.StringValue(region) - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "ObjectStorage credentials group read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *credentialsGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials group", "CredentialsGroup can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialsGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - credentialsGroupId := model.CredentialsGroupId.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "credentials_group_id", credentialsGroupId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing credentials group - _, err := r.client.DeleteCredentialsGroup(ctx, projectId, region, credentialsGroupId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credentials group", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "ObjectStorage credentials group deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id, credentials_group_id -func (r *credentialsGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credentialsGroup", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[credentials_group_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credentials_group_id"), idParts[2])...) - tflog.Info(ctx, "ObjectStorage credentials group state imported") -} - -func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupResponse, model *Model, region string) error { - if credentialsGroupResp == nil { - return fmt.Errorf("response input is nil") - } - if credentialsGroupResp.CredentialsGroup == nil { - return fmt.Errorf("response credentialsGroup is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - credentialsGroup := credentialsGroupResp.CredentialsGroup - - err := mapCredentialsGroup(*credentialsGroup, model, region) - if err != nil { - return err - } - model.Region = types.StringValue(region) - return nil -} - -func mapCredentialsGroup(credentialsGroup objectstorage.CredentialsGroup, model *Model, region string) error { - var credentialsGroupId string - if !utils.IsUndefined(model.CredentialsGroupId) { - credentialsGroupId = model.CredentialsGroupId.ValueString() - } else if credentialsGroup.CredentialsGroupId != nil { - credentialsGroupId = *credentialsGroup.CredentialsGroupId - } else { - return fmt.Errorf("credential id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, credentialsGroupId) - model.CredentialsGroupId = types.StringValue(credentialsGroupId) - model.URN = types.StringPointerValue(credentialsGroup.Urn) - model.Name = types.StringPointerValue(credentialsGroup.DisplayName) - return nil -} - -type objectStorageClient interface { - EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error) - ListCredentialsGroupsExecute(ctx context.Context, projectId, region string) (*objectstorage.ListCredentialsGroupsResponse, error) -} - -// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens -func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error { - projectId := model.ProjectId.ValueString() - - // From the object storage OAS: Creation will also be successful if the project is already enabled, but will not create a duplicate - _, err := client.EnableServiceExecute(ctx, projectId, region) - if err != nil { - return fmt.Errorf("failed to create object storage project: %w", err) - } - return nil -} - -// readCredentialsGroups gets all the existing credentials groups for the specified project, -// finds the credentials group that is being read and updates the state. -// Returns True if the credential was found, False otherwise. -func readCredentialsGroups(ctx context.Context, model *Model, region string, client objectStorageClient) (bool, error) { - found := false - - if model.CredentialsGroupId.ValueString() == "" && model.Name.ValueString() == "" { - return found, fmt.Errorf("missing configuration: either name or credentials group id must be provided") - } - - credentialsGroupsResp, err := client.ListCredentialsGroupsExecute(ctx, model.ProjectId.ValueString(), region) - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - return found, nil - } - return found, fmt.Errorf("getting credentials groups: %w", err) - } - - if credentialsGroupsResp == nil { - return found, fmt.Errorf("nil response from GET credentials groups") - } - - for _, credentialsGroup := range *credentialsGroupsResp.CredentialsGroups { - if *credentialsGroup.CredentialsGroupId != model.CredentialsGroupId.ValueString() && *credentialsGroup.DisplayName != model.Name.ValueString() { - continue - } - found = true - err = mapCredentialsGroup(credentialsGroup, model, region) - if err != nil { - return found, err - } - break - } - - return found, nil -} diff --git a/stackit/internal/services/objectstorage/credentialsgroup/resource_test.go b/stackit/internal/services/objectstorage/credentialsgroup/resource_test.go deleted file mode 100644 index 37b0dae8..00000000 --- a/stackit/internal/services/objectstorage/credentialsgroup/resource_test.go +++ /dev/null @@ -1,317 +0,0 @@ -package objectstorage - -import ( - "context" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) - -type objectStorageClientMocked struct { - returnError bool - listCredentialsGroupsResp *objectstorage.ListCredentialsGroupsResponse -} - -func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, projectId, _ string) (*objectstorage.ProjectStatus, error) { - if c.returnError { - return nil, fmt.Errorf("create project failed") - } - - return &objectstorage.ProjectStatus{ - Project: utils.Ptr(projectId), - }, nil -} - -func (c *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("get credentials groups failed") - } - - return c.listCredentialsGroupsResp, nil -} - -func TestMapFields(t *testing.T) { - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cid") - tests := []struct { - description string - input *objectstorage.CreateCredentialsGroupResponse - expected Model - isValid bool - }{ - { - "default_values", - &objectstorage.CreateCredentialsGroupResponse{ - CredentialsGroup: &objectstorage.CredentialsGroup{}, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringNull(), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cid"), - URN: types.StringNull(), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "simple_values", - &objectstorage.CreateCredentialsGroupResponse{ - CredentialsGroup: &objectstorage.CredentialsGroup{ - DisplayName: utils.Ptr("name"), - Urn: utils.Ptr("urn"), - }, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringValue("name"), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cid"), - URN: types.StringValue("urn"), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "empty_strings", - &objectstorage.CreateCredentialsGroupResponse{ - CredentialsGroup: &objectstorage.CredentialsGroup{ - DisplayName: utils.Ptr(""), - Urn: utils.Ptr(""), - }, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringValue(""), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cid"), - URN: types.StringValue(""), - Region: types.StringValue("eu01"), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_bucket", - &objectstorage.CreateCredentialsGroupResponse{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - CredentialsGroupId: tt.expected.CredentialsGroupId, - } - err := mapFields(tt.input, model, "eu01") - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestEnableProject(t *testing.T) { - tests := []struct { - description string - enableFails bool - isValid bool - }{ - { - "default_values", - false, - true, - }, - { - "error_response", - true, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &objectStorageClientMocked{ - returnError: tt.enableFails, - } - err := enableProject(context.Background(), &Model{}, "eu01", client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - }) - } -} - -func TestReadCredentialsGroups(t *testing.T) { - const testRegion = "eu01" - id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cid") - tests := []struct { - description string - mockedResp *objectstorage.ListCredentialsGroupsResponse - expectedModel Model - expectedFound bool - getCredentialsGroupsFails bool - isValid bool - }{ - { - "default_values", - &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ - { - CredentialsGroupId: utils.Ptr("cid"), - }, - { - CredentialsGroupId: utils.Ptr("foo-id"), - }, - }, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringNull(), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cid"), - URN: types.StringNull(), - }, - true, - false, - true, - }, - { - "simple_values", - &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ - { - CredentialsGroupId: utils.Ptr("cid"), - DisplayName: utils.Ptr("name"), - Urn: utils.Ptr("urn"), - }, - { - CredentialsGroupId: utils.Ptr("foo-cid"), - DisplayName: utils.Ptr("foo-name"), - Urn: utils.Ptr("foo-urn"), - }, - }, - }, - Model{ - Id: types.StringValue(id), - Name: types.StringValue("name"), - ProjectId: types.StringValue("pid"), - CredentialsGroupId: types.StringValue("cid"), - URN: types.StringValue("urn"), - }, - true, - false, - true, - }, - { - "empty_credentials_groups", - &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{}, - }, - Model{}, - false, - false, - false, - }, - { - "nil_credentials_groups", - &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: nil, - }, - Model{}, - false, - false, - false, - }, - { - "nil_response", - nil, - Model{}, - false, - false, - false, - }, - { - "non_matching_credentials_group", - &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ - { - CredentialsGroupId: utils.Ptr("foo-other"), - DisplayName: utils.Ptr("foo-name"), - Urn: utils.Ptr("foo-urn"), - }, - }, - }, - Model{}, - false, - false, - false, - }, - { - "error_response", - &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ - { - CredentialsGroupId: utils.Ptr("other_id"), - DisplayName: utils.Ptr("name"), - Urn: utils.Ptr("urn"), - }, - }, - }, - Model{}, - false, - true, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &objectStorageClientMocked{ - returnError: tt.getCredentialsGroupsFails, - listCredentialsGroupsResp: tt.mockedResp, - } - model := &Model{ - ProjectId: tt.expectedModel.ProjectId, - CredentialsGroupId: tt.expectedModel.CredentialsGroupId, - } - found, err := readCredentialsGroups(context.Background(), model, "eu01", client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - - if found != tt.expectedFound { - t.Fatalf("Found does not match") - } - } - }) - } -} diff --git a/stackit/internal/services/objectstorage/objectstorage_acc_test.go b/stackit/internal/services/objectstorage/objectstorage_acc_test.go deleted file mode 100644 index 4fd11725..00000000 --- a/stackit/internal/services/objectstorage/objectstorage_acc_test.go +++ /dev/null @@ -1,316 +0,0 @@ -package objectstorage_test - -import ( - "context" - _ "embed" - "fmt" - "strings" - "testing" - "time" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -//go:embed testfiles/resource-min.tf -var resourceMinConfig string - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "objectstorage_bucket_name": config.StringVariable(fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(20, acctest.CharSetAlpha))), - "objectstorage_credentials_group_name": config.StringVariable(fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(20, acctest.CharSetAlpha))), - "expiration_timestamp": config.StringVariable(fmt.Sprintf("%d-01-02T03:04:05Z", time.Now().Year()+1)), -} - -func TestAccObjectStorageResourceMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckObjectStorageDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMin, - Config: testutil.ObjectStorageProviderConfig() + resourceMinConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Bucket data - resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket", "name", testutil.ConvertConfigVariable(testConfigVarsMin["objectstorage_bucket_name"])), - resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket", "url_path_style"), - resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket", "url_virtual_hosted_style"), - - // Credentials group data - resource.TestCheckResourceAttr("stackit_objectstorage_credentials_group.credentials_group", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_objectstorage_credentials_group.credentials_group", "name", testutil.ConvertConfigVariable(testConfigVarsMin["objectstorage_credentials_group_name"])), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credentials_group.credentials_group", "credentials_group_id"), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credentials_group.credentials_group", "urn"), - - // Credential data - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential", "project_id", - "stackit_objectstorage_credentials_group.credentials_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential", "credentials_group_id", - "stackit_objectstorage_credentials_group.credentials_group", "credentials_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "name"), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "access_key"), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential", "secret_access_key"), - - // credential_time data - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential_time", "project_id", - "stackit_objectstorage_credentials_group.credentials_group", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential_time", "credentials_group_id", - "stackit_objectstorage_credentials_group.credentials_group", "credentials_group_id", - ), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential_time", "credential_id"), - resource.TestCheckResourceAttr("stackit_objectstorage_credential.credential_time", "expiration_timestamp", testutil.ConvertConfigVariable(testConfigVarsMin["expiration_timestamp"])), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential_time", "name"), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential_time", "access_key"), - resource.TestCheckResourceAttrSet("stackit_objectstorage_credential.credential_time", "secret_access_key"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMin, - Config: fmt.Sprintf(` - %s - - data "stackit_objectstorage_bucket" "bucket" { - project_id = stackit_objectstorage_bucket.bucket.project_id - name = stackit_objectstorage_bucket.bucket.name - } - - data "stackit_objectstorage_credentials_group" "credentials_group" { - project_id = stackit_objectstorage_credentials_group.credentials_group.project_id - credentials_group_id = stackit_objectstorage_credentials_group.credentials_group.credentials_group_id - } - - data "stackit_objectstorage_credential" "credential" { - project_id = stackit_objectstorage_credential.credential.project_id - credentials_group_id = stackit_objectstorage_credential.credential.credentials_group_id - credential_id = stackit_objectstorage_credential.credential.credential_id - } - - data "stackit_objectstorage_credential" "credential_time" { - project_id = stackit_objectstorage_credential.credential_time.project_id - credentials_group_id = stackit_objectstorage_credential.credential_time.credentials_group_id - credential_id = stackit_objectstorage_credential.credential_time.credential_id - }`, - testutil.ObjectStorageProviderConfig()+resourceMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Bucket data - resource.TestCheckResourceAttr("data.stackit_objectstorage_bucket.bucket", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_bucket.bucket", "name", - "data.stackit_objectstorage_bucket.bucket", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_bucket.bucket", "url_path_style", - "data.stackit_objectstorage_bucket.bucket", "url_path_style", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_bucket.bucket", "url_virtual_hosted_style", - "data.stackit_objectstorage_bucket.bucket", "url_virtual_hosted_style", - ), - - // Credentials group data - resource.TestCheckResourceAttr("data.stackit_objectstorage_credentials_group.credentials_group", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credentials_group.credentials_group", "credentials_group_id", - "data.stackit_objectstorage_credentials_group.credentials_group", "credentials_group_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credentials_group.credentials_group", "name", - "data.stackit_objectstorage_credentials_group.credentials_group", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credentials_group.credentials_group", "urn", - "data.stackit_objectstorage_credentials_group.credentials_group", "urn", - ), - - // Credential data - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential", "project_id", - "data.stackit_objectstorage_credential.credential", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential", "credentials_group_id", - "data.stackit_objectstorage_credential.credential", "credentials_group_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential", "credential_id", - "data.stackit_objectstorage_credential.credential", "credential_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential", "name", - "data.stackit_objectstorage_credential.credential", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential", "expiration_timestamp", - "data.stackit_objectstorage_credential.credential", "expiration_timestamp", - ), - - // Credential_time data - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential_time", "project_id", - "data.stackit_objectstorage_credential.credential_time", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential_time", "credentials_group_id", - "data.stackit_objectstorage_credential.credential_time", "credentials_group_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential_time", "credential_id", - "data.stackit_objectstorage_credential.credential_time", "credential_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential_time", "name", - "data.stackit_objectstorage_credential.credential_time", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_objectstorage_credential.credential_time", "expiration_timestamp", - "data.stackit_objectstorage_credential.credential_time", "expiration_timestamp", - ), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_objectstorage_credentials_group.credentials_group", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_objectstorage_credentials_group.credentials_group"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_objectstorage_credentials_group.credentials_group") - } - credentialsGroupId, ok := r.Primary.Attributes["credentials_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credentials_group_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, credentialsGroupId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_objectstorage_credential.credential", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_objectstorage_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_objectstorage_credential.credential") - } - credentialsGroupId, ok := r.Primary.Attributes["credentials_group_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credentials_group_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, credentialsGroupId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"access_key", "secret_access_key"}, - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckObjectStorageDestroy(s *terraform.State) error { - ctx := context.Background() - var client *objectstorage.APIClient - var err error - if testutil.ObjectStorageCustomEndpoint == "" { - client, err = objectstorage.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) - } else { - client, err = objectstorage.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.ObjectStorageCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - bucketsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_objectstorage_bucket" { - continue - } - // bucket terraform ID: "[project_id],[name]" - bucketName := strings.Split(rs.Primary.ID, core.Separator)[1] - bucketsToDestroy = append(bucketsToDestroy, bucketName) - } - - bucketsResp, err := client.ListBuckets(ctx, testutil.ProjectId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting bucketsResp: %w", err) - } - - buckets := *bucketsResp.Buckets - for _, bucket := range buckets { - if bucket.Name == nil { - continue - } - bucketName := *bucket.Name - if utils.Contains(bucketsToDestroy, bucketName) { - _, err := client.DeleteBucketExecute(ctx, testutil.ProjectId, testutil.Region, bucketName) - if err != nil { - return fmt.Errorf("destroying bucket %s during CheckDestroy: %w", bucketName, err) - } - _, err = wait.DeleteBucketWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, bucketName).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", bucketName, err) - } - } - } - - credentialsGroupsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_objectstorage_credentials_group" { - continue - } - // credentials group terraform ID: "[project_id],[credentials_group_id]" - credentialsGroupId := strings.Split(rs.Primary.ID, core.Separator)[1] - credentialsGroupsToDestroy = append(credentialsGroupsToDestroy, credentialsGroupId) - } - - credentialsGroupsResp, err := client.ListCredentialsGroups(ctx, testutil.ProjectId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting bucketsResp: %w", err) - } - - groups := *credentialsGroupsResp.CredentialsGroups - for _, group := range groups { - if group.CredentialsGroupId == nil { - continue - } - groupId := *group.CredentialsGroupId - if utils.Contains(credentialsGroupsToDestroy, groupId) { - _, err := client.DeleteCredentialsGroupExecute(ctx, testutil.ProjectId, testutil.Region, groupId) - if err != nil { - return fmt.Errorf("destroying credentials group %s during CheckDestroy: %w", groupId, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/objectstorage/testfiles/resource-min.tf b/stackit/internal/services/objectstorage/testfiles/resource-min.tf deleted file mode 100644 index db9b28fa..00000000 --- a/stackit/internal/services/objectstorage/testfiles/resource-min.tf +++ /dev/null @@ -1,26 +0,0 @@ - -variable "project_id" {} -variable "objectstorage_bucket_name" {} -variable "objectstorage_credentials_group_name" {} -variable "expiration_timestamp" {} - -resource "stackit_objectstorage_bucket" "bucket" { - project_id = var.project_id - name = var.objectstorage_bucket_name -} - -resource "stackit_objectstorage_credentials_group" "credentials_group" { - project_id = var.project_id - name = var.objectstorage_credentials_group_name -} - -resource "stackit_objectstorage_credential" "credential" { - project_id = stackit_objectstorage_credentials_group.credentials_group.project_id - credentials_group_id = stackit_objectstorage_credentials_group.credentials_group.credentials_group_id -} - -resource "stackit_objectstorage_credential" "credential_time" { - project_id = stackit_objectstorage_credentials_group.credentials_group.project_id - credentials_group_id = stackit_objectstorage_credentials_group.credentials_group.credentials_group_id - expiration_timestamp = var.expiration_timestamp -} diff --git a/stackit/internal/services/objectstorage/utils/util.go b/stackit/internal/services/objectstorage/utils/util.go deleted file mode 100644 index 56a013a2..00000000 --- a/stackit/internal/services/objectstorage/utils/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *objectstorage.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ObjectStorageCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ObjectStorageCustomEndpoint)) - } - apiClient, err := objectstorage.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/objectstorage/utils/util_test.go b/stackit/internal/services/objectstorage/utils/util_test.go deleted file mode 100644 index d31dc8a6..00000000 --- a/stackit/internal/services/objectstorage/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://objectstorage-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *objectstorage.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *objectstorage.APIClient { - apiClient, err := objectstorage.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ObjectStorageCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *objectstorage.APIClient { - apiClient, err := objectstorage.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/observability/alertgroup/datasource.go b/stackit/internal/services/observability/alertgroup/datasource.go deleted file mode 100644 index 998151e5..00000000 --- a/stackit/internal/services/observability/alertgroup/datasource.go +++ /dev/null @@ -1,173 +0,0 @@ -package alertgroup - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &alertGroupDataSource{} -) - -// NewAlertGroupDataSource creates a new instance of the alertGroupDataSource. -func NewAlertGroupDataSource() datasource.DataSource { - return &alertGroupDataSource{} -} - -// alertGroupDataSource is the datasource implementation. -type alertGroupDataSource struct { - client *observability.APIClient -} - -// Configure adds the provider configured client to the resource. -func (a *alertGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - a.client = apiClient - tflog.Info(ctx, "Observability alert group client configured") -} - -// Metadata provides metadata for the alert group datasource. -func (a *alertGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_alertgroup" -} - -// Schema defines the schema for the alert group data source. -func (a *alertGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability alert group datasource schema. Used to create alerts based on metrics (Thanos). Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - stringvalidator.LengthBetween(1, 200), - }, - }, - "interval": schema.StringAttribute{ - Description: descriptions["interval"], - Computed: true, - Validators: []validator.String{ - validate.ValidDurationString(), - }, - }, - "rules": schema.ListNestedAttribute{ - Description: descriptions["rules"], - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "alert": schema.StringAttribute{ - Description: descriptions["alert"], - Computed: true, - }, - "expression": schema.StringAttribute{ - Description: descriptions["expression"], - Computed: true, - }, - "for": schema.StringAttribute{ - Description: descriptions["for"], - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - ElementType: types.StringType, - Computed: true, - }, - "annotations": schema.MapAttribute{ - Description: descriptions["annotations"], - ElementType: types.StringType, - Computed: true, - }, - }, - }, - }, - }, - } -} - -func (a *alertGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - readAlertGroupResp, err := a.client.GetAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading alert group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, readAlertGroupResp.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading alert group", fmt.Sprintf("Error processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) -} diff --git a/stackit/internal/services/observability/alertgroup/resource.go b/stackit/internal/services/observability/alertgroup/resource.go deleted file mode 100644 index e91d9f68..00000000 --- a/stackit/internal/services/observability/alertgroup/resource.go +++ /dev/null @@ -1,574 +0,0 @@ -package alertgroup - -import ( - "context" - "errors" - "fmt" - "net/http" - "regexp" - "strings" - - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "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/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &alertGroupResource{} - _ resource.ResourceWithConfigure = &alertGroupResource{} - _ resource.ResourceWithImportState = &alertGroupResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - InstanceId types.String `tfsdk:"instance_id"` - Name types.String `tfsdk:"name"` - Interval types.String `tfsdk:"interval"` - Rules types.List `tfsdk:"rules"` -} - -type rule struct { - Alert types.String `tfsdk:"alert"` - Annotations types.Map `tfsdk:"annotations"` - Labels types.Map `tfsdk:"labels"` - Expression types.String `tfsdk:"expression"` - For types.String `tfsdk:"for"` -} - -var ruleTypes = map[string]attr.Type{ - "alert": basetypes.StringType{}, - "annotations": basetypes.MapType{ElemType: types.StringType}, - "labels": basetypes.MapType{ElemType: types.StringType}, - "expression": basetypes.StringType{}, - "for": basetypes.StringType{}, -} - -// Descriptions for the resource and data source schemas are centralized here. -var descriptions = map[string]string{ - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`name`\".", - "project_id": "STACKIT project ID to which the alert group is associated.", - "instance_id": "Observability instance ID to which the alert group is associated.", - "name": "The name of the alert group. Is the identifier and must be unique in the group.", - "interval": "Specifies the frequency at which rules within the group are evaluated. The interval must be at least 60 seconds and defaults to 60 seconds if not set. Supported formats include hours, minutes, and seconds, either singly or in combination. Examples of valid formats are: '5h30m40s', '5h', '5h30m', '60m', and '60s'.", - "alert": "The name of the alert rule. Is the identifier and must be unique in the group.", - "expression": "The PromQL expression to evaluate. Every evaluation cycle this is evaluated at the current time, and all resultant time series become pending/firing alerts.", - "for": "Alerts are considered firing once they have been returned for this long. Alerts which have not yet fired for long enough are considered pending. Default is 0s", - "labels": "A map of key:value. Labels to add or overwrite for each alert", - "annotations": "A map of key:value. Annotations to add or overwrite for each alert", -} - -// NewAlertGroupResource is a helper function to simplify the provider implementation. -func NewAlertGroupResource() resource.Resource { - return &alertGroupResource{} -} - -// alertGroupResource is the resource implementation. -type alertGroupResource struct { - client *observability.APIClient -} - -// Metadata returns the resource type name. -func (a *alertGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_alertgroup" -} - -// Configure adds the provider configured client to the resource. -func (a *alertGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - a.client = apiClient - tflog.Info(ctx, "Observability alert group client configured") -} - -// Schema defines the schema for the resource. -func (a *alertGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability alert group resource schema. Used to create alerts based on metrics (Thanos). Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - stringvalidator.LengthBetween(1, 200), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[a-zA-Z0-9-]+$`), - "must match expression", - ), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "interval": schema.StringAttribute{ - Description: descriptions["interval"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.ValidDurationString(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "rules": schema.ListNestedAttribute{ - Description: "Rules for the alert group", - Required: true, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "alert": schema.StringAttribute{ - Description: descriptions["alert"], - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile(`^[a-zA-Z0-9-]+$`), - "must match expression", - ), - stringvalidator.LengthBetween(1, 200), - }, - }, - "expression": schema.StringAttribute{ - Description: descriptions["expression"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 600), - // The API currently accepts expressions with trailing newlines but does not return them, - // leading to inconsistent Terraform results. This issue has been reported to the Obs team. - // Until it is resolved, we proactively notify users if their input contains a trailing newline. - validate.ValidNoTrailingNewline(), - }, - }, - "for": schema.StringAttribute{ - Description: descriptions["for"], - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(2, 8), - validate.ValidDurationString(), - }, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - Optional: true, - ElementType: types.StringType, - Validators: []validator.Map{ - mapvalidator.KeysAre(stringvalidator.LengthAtMost(200)), - mapvalidator.ValueStringsAre(stringvalidator.LengthAtMost(200)), - mapvalidator.SizeAtMost(10), - }, - }, - "annotations": schema.MapAttribute{ - Description: descriptions["annotations"], - Optional: true, - ElementType: types.StringType, - Validators: []validator.Map{ - mapvalidator.KeysAre(stringvalidator.LengthAtMost(200)), - mapvalidator.ValueStringsAre(stringvalidator.LengthAtMost(200)), - mapvalidator.SizeAtMost(5), - }, - }, - }, - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (a *alertGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating alertgroup", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - createAlertGroupResp, err := a.client.CreateAlertgroups(ctx, instanceId, projectId).CreateAlertgroupsPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating alertgroup", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // all alert groups are returned. We have to search the map for the one corresponding to our name - for _, alertGroup := range *createAlertGroupResp.Data { - if model.Name.ValueString() != *alertGroup.Name { - continue - } - - err = mapFields(ctx, &alertGroup, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating alert group", fmt.Sprintf("Processing API payload: %v", err)) - return - } - } - - // Set the state with fully populated data. - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "alert group created") -} - -// Read refreshes the Terraform state with the latest data. -func (a *alertGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - readAlertGroupResp, err := a.client.GetAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading alert group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, readAlertGroupResp.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading alert group", fmt.Sprintf("Error processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) -} - -// Update attempts to update the resource. In this case, alertgroups cannot be updated. -// The Update function is redundant since any modifications will -// automatically trigger a resource recreation through Terraform's built-in -// lifecycle management. -func (a *alertGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating alert group", "Observability alert groups can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (a *alertGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - _, err := a.client.DeleteAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting alert group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Alert group deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,name -func (a *alertGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing scrape config", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id],[name] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "Observability alert group state imported") -} - -// toCreatePayload generates the payload to create a new alert group. -func toCreatePayload(ctx context.Context, model *Model) (*observability.CreateAlertgroupsPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payload := observability.CreateAlertgroupsPayload{} - - if !utils.IsUndefined(model.Name) { - payload.Name = model.Name.ValueStringPointer() - } - - if !utils.IsUndefined(model.Interval) { - payload.Interval = model.Interval.ValueStringPointer() - } - - if !utils.IsUndefined(model.Rules) { - rules, err := toRulesPayload(ctx, model) - if err != nil { - return nil, err - } - payload.Rules = &rules - } - - return &payload, nil -} - -// toRulesPayload generates rules for create payload. -func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAlertgroupsRequestInnerRulesInner, error) { - if model.Rules.Elements() == nil || len(model.Rules.Elements()) == 0 { - return []observability.UpdateAlertgroupsRequestInnerRulesInner{}, nil - } - - var rules []rule - diags := model.Rules.ElementsAs(ctx, &rules, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - var oarrs []observability.UpdateAlertgroupsRequestInnerRulesInner - for i := range rules { - rule := &rules[i] - oarr := observability.UpdateAlertgroupsRequestInnerRulesInner{} - - if !utils.IsUndefined(rule.Alert) { - alert := conversion.StringValueToPointer(rule.Alert) - if alert == nil { - return nil, fmt.Errorf("found nil alert for rule[%d]", i) - } - oarr.Alert = alert - } - - if !utils.IsUndefined(rule.Expression) { - expression := conversion.StringValueToPointer(rule.Expression) - if expression == nil { - return nil, fmt.Errorf("found nil expression for rule[%d]", i) - } - oarr.Expr = expression - } - - if !utils.IsUndefined(rule.For) { - for_ := conversion.StringValueToPointer(rule.For) - if for_ == nil { - return nil, fmt.Errorf("found nil expression for for_[%d]", i) - } - oarr.For = for_ - } - - if !utils.IsUndefined(rule.Labels) { - labels, err := conversion.ToStringInterfaceMap(ctx, rule.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - oarr.Labels = &labels - } - - if !utils.IsUndefined(rule.Annotations) { - annotations, err := conversion.ToStringInterfaceMap(ctx, rule.Annotations) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - oarr.Annotations = &annotations - } - - oarrs = append(oarrs, oarr) - } - - return oarrs, nil -} - -// mapRules maps alertGroup response to the model. -func mapFields(ctx context.Context, alertGroup *observability.AlertGroup, model *Model) error { - if alertGroup == nil { - return fmt.Errorf("nil alertGroup") - } - - if model == nil { - return fmt.Errorf("nil model") - } - - if utils.IsUndefined(model.Name) { - return fmt.Errorf("empty name") - } - - if utils.IsUndefined(model.ProjectId) { - return fmt.Errorf("empty projectId") - } - - if utils.IsUndefined(model.InstanceId) { - return fmt.Errorf("empty instanceId") - } - - var name string - if !utils.IsUndefined(model.Name) { - name = model.Name.ValueString() - } else if alertGroup.Name != nil { - name = *alertGroup.Name - } else { - return fmt.Errorf("found empty name") - } - - model.Name = types.StringValue(name) - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), name) - - var interval string - if !utils.IsUndefined(model.Interval) { - interval = model.Interval.ValueString() - } else if alertGroup.Interval != nil { - interval = *alertGroup.Interval - } else { - return fmt.Errorf("found empty interval") - } - model.Interval = types.StringValue(interval) - - if alertGroup.Rules != nil { - err := mapRules(ctx, alertGroup, model) - if err != nil { - return fmt.Errorf("map rules: %w", err) - } - } - - return nil -} - -// mapRules maps alertGroup response rules to the model rules. -func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Model) error { - var newRules []attr.Value - - for i, r := range *alertGroup.Rules { - ruleMap := map[string]attr.Value{ - "alert": types.StringPointerValue(r.Alert), - "expression": types.StringPointerValue(r.Expr), - "for": types.StringPointerValue(r.For), - "labels": types.MapNull(types.StringType), - "annotations": types.MapNull(types.StringType), - } - - if r.Labels != nil { - labelElems := map[string]attr.Value{} - for k, v := range *r.Labels { - labelElems[k] = types.StringValue(v) - } - ruleMap["labels"] = types.MapValueMust(types.StringType, labelElems) - } - - if r.Annotations != nil { - annoElems := map[string]attr.Value{} - for k, v := range *r.Annotations { - annoElems[k] = types.StringValue(v) - } - ruleMap["annotations"] = types.MapValueMust(types.StringType, annoElems) - } - - ruleTf, diags := types.ObjectValue(ruleTypes, ruleMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - newRules = append(newRules, ruleTf) - } - - rulesTf, diags := types.ListValue(types.ObjectType{AttrTypes: ruleTypes}, newRules) - if diags.HasError() { - return core.DiagsToError(diags) - } - - model.Rules = rulesTf - return nil -} diff --git a/stackit/internal/services/observability/alertgroup/resource_test.go b/stackit/internal/services/observability/alertgroup/resource_test.go deleted file mode 100644 index 74697421..00000000 --- a/stackit/internal/services/observability/alertgroup/resource_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package alertgroup - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" -) - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - name string - input *Model - expect *observability.CreateAlertgroupsPayload - expectErr bool - }{ - { - name: "Nil Model", - input: nil, - expect: nil, - expectErr: true, - }, - { - name: "Empty Model", - input: &Model{ - Name: types.StringNull(), - Interval: types.StringNull(), - Rules: types.ListNull(types.StringType), - }, - expect: &observability.CreateAlertgroupsPayload{}, - expectErr: false, - }, - { - name: "Model with Name and Interval", - input: &Model{ - Name: types.StringValue("test-alertgroup"), - Interval: types.StringValue("5m"), - }, - expect: &observability.CreateAlertgroupsPayload{ - Name: utils.Ptr("test-alertgroup"), - Interval: utils.Ptr("5m"), - }, - expectErr: false, - }, - { - name: "Model with Full Information", - input: &Model{ - Name: types.StringValue("full-alertgroup"), - Interval: types.StringValue("10m"), - Rules: types.ListValueMust( - types.ObjectType{AttrTypes: ruleTypes}, - []attr.Value{ - types.ObjectValueMust( - ruleTypes, - map[string]attr.Value{ - "alert": types.StringValue("alert"), - "expression": types.StringValue("expression"), - "for": types.StringValue("10s"), - "labels": types.MapValueMust( - types.StringType, - map[string]attr.Value{ - "k": types.StringValue("v"), - }, - ), - "annotations": types.MapValueMust( - types.StringType, - map[string]attr.Value{ - "k": types.StringValue("v"), - }, - ), - }, - ), - }, - ), - }, - expect: &observability.CreateAlertgroupsPayload{ - Name: utils.Ptr("full-alertgroup"), - Interval: utils.Ptr("10m"), - Rules: &[]observability.UpdateAlertgroupsRequestInnerRulesInner{ - { - Alert: utils.Ptr("alert"), - Annotations: &map[string]interface{}{ - "k": "v", - }, - Expr: utils.Ptr("expression"), - For: utils.Ptr("10s"), - Labels: &map[string]interface{}{ - "k": "v", - }, - }, - }, - }, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toCreatePayload(ctx, tt.input) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err) - } - - if diff := cmp.Diff(got, tt.expect); diff != "" { - t.Errorf("unexpected result (-got +want):\n%s", diff) - } - }) - } -} - -func TestToRulesPayload(t *testing.T) { - tests := []struct { - name string - input *Model - expect []observability.UpdateAlertgroupsRequestInnerRulesInner - expectErr bool - }{ - { - name: "Nil Rules", - input: &Model{ - Rules: types.ListNull(types.StringType), // Simulates a lack of rules - }, - expect: []observability.UpdateAlertgroupsRequestInnerRulesInner{}, - expectErr: false, - }, - { - name: "Invalid Rule Element Type", - input: &Model{ - Rules: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("invalid"), // Should cause a conversion failure - }), - }, - expect: nil, - expectErr: true, - }, - { - name: "Single Valid Rule", - input: &Model{ - Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleTypes}, []attr.Value{ - types.ObjectValueMust(ruleTypes, map[string]attr.Value{ - "alert": types.StringValue("alert"), - "expression": types.StringValue("expr"), - "for": types.StringValue("5s"), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - "annotations": types.MapValueMust(types.StringType, map[string]attr.Value{ - "note": types.StringValue("important"), - }), - }), - }), - }, - expect: []observability.UpdateAlertgroupsRequestInnerRulesInner{ - { - Alert: utils.Ptr("alert"), - Expr: utils.Ptr("expr"), - For: utils.Ptr("5s"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Annotations: &map[string]interface{}{ - "note": "important", - }, - }, - }, - expectErr: false, - }, - { - name: "Multiple Valid Rules", - input: &Model{ - Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleTypes}, []attr.Value{ - types.ObjectValueMust(ruleTypes, map[string]attr.Value{ - "alert": types.StringValue("alert1"), - "expression": types.StringValue("expr1"), - "for": types.StringValue("5s"), - "labels": types.MapNull(types.StringType), - "annotations": types.MapNull(types.StringType), - }), - types.ObjectValueMust(ruleTypes, map[string]attr.Value{ - "alert": types.StringValue("alert2"), - "expression": types.StringValue("expr2"), - "for": types.StringValue("10s"), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - "annotations": types.MapValueMust(types.StringType, map[string]attr.Value{ - "note": types.StringValue("important"), - }), - }), - }), - }, - expect: []observability.UpdateAlertgroupsRequestInnerRulesInner{ - { - Alert: utils.Ptr("alert1"), - Expr: utils.Ptr("expr1"), - For: utils.Ptr("5s"), - }, - { - Alert: utils.Ptr("alert2"), - Expr: utils.Ptr("expr2"), - For: utils.Ptr("10s"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Annotations: &map[string]interface{}{ - "note": "important", - }, - }, - }, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toRulesPayload(ctx, tt.input) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err) - } - - if diff := cmp.Diff(got, tt.expect); diff != "" { - t.Errorf("unexpected result (-got +want):\n%s", diff) - } - }) - } -} - -func TestMapFields(t *testing.T) { - tests := []struct { - name string - alertGroup *observability.AlertGroup - model *Model - expectedName string - expectedID string - expectErr bool - }{ - { - name: "Nil AlertGroup", - alertGroup: nil, - model: &Model{}, - expectErr: true, - }, - { - name: "Nil Model", - alertGroup: &observability.AlertGroup{}, - model: nil, - expectErr: true, - }, - { - name: "Interval Missing", - alertGroup: &observability.AlertGroup{ - Name: utils.Ptr("alert-group-name"), - }, - model: &Model{ - Name: types.StringValue("alert-group-name"), - ProjectId: types.StringValue("project1"), - InstanceId: types.StringValue("instance1"), - }, - expectedName: "alert-group-name", - expectedID: "project1,instance1,alert-group-name", - expectErr: true, - }, - { - name: "Name Missing", - alertGroup: &observability.AlertGroup{ - Interval: utils.Ptr("5m"), - }, - model: &Model{ - Name: types.StringValue("model-name"), - InstanceId: types.StringValue("instance1"), - }, - expectErr: true, - }, - { - name: "Complete Model and AlertGroup", - alertGroup: &observability.AlertGroup{ - Name: utils.Ptr("alert-group-name"), - Interval: utils.Ptr("10m"), - }, - model: &Model{ - Name: types.StringValue("alert-group-name"), - ProjectId: types.StringValue("project1"), - InstanceId: types.StringValue("instance1"), - Id: types.StringValue("project1,instance1,alert-group-name"), - Interval: types.StringValue("10m"), - }, - expectedName: "alert-group-name", - expectedID: "project1,instance1,alert-group-name", - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - err := mapFields(ctx, tt.alertGroup, tt.model) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err) - } - - if !tt.expectErr { - if diff := cmp.Diff(tt.model.Name.ValueString(), tt.expectedName); diff != "" { - t.Errorf("unexpected name (-got +want):\n%s", diff) - } - if diff := cmp.Diff(tt.model.Id.ValueString(), tt.expectedID); diff != "" { - t.Errorf("unexpected ID (-got +want):\n%s", diff) - } - } - }) - } -} - -func TestMapRules(t *testing.T) { - tests := []struct { - name string - alertGroup *observability.AlertGroup - model *Model - expectErr bool - }{ - { - name: "Empty Rules", - alertGroup: &observability.AlertGroup{ - Rules: &[]observability.AlertRuleRecord{}, - }, - model: &Model{}, - expectErr: false, - }, - { - name: "Single Complete Rule", - alertGroup: &observability.AlertGroup{ - Rules: &[]observability.AlertRuleRecord{ - { - Alert: utils.Ptr("HighCPUUsage"), - Expr: utils.Ptr("rate(cpu_usage[5m]) > 0.9"), - For: utils.Ptr("2m"), - Labels: &map[string]string{"severity": "critical"}, - Annotations: &map[string]string{"summary": "CPU usage high"}, - Record: utils.Ptr("record1"), - }, - }, - }, - model: &Model{}, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - err := mapRules(ctx, tt.alertGroup, tt.model) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err != nil) - } - }) - } -} diff --git a/stackit/internal/services/observability/credential/resource.go b/stackit/internal/services/observability/credential/resource.go deleted file mode 100644 index fc989d87..00000000 --- a/stackit/internal/services/observability/credential/resource.go +++ /dev/null @@ -1,262 +0,0 @@ -package observability - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialResource{} - _ resource.ResourceWithConfigure = &credentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - InstanceId types.String `tfsdk:"instance_id"` - Description types.String `tfsdk:"description"` - Username types.String `tfsdk:"username"` - Password types.String `tfsdk:"password"` -} - -// NewCredentialResource is a helper function to simplify the provider implementation. -func NewCredentialResource() resource.Resource { - return &credentialResource{} -} - -// credentialResource is the resource implementation. -type credentialResource struct { - client *observability.APIClient -} - -// Metadata returns the resource type name. -func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_credential" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Observability credential client configured") -} - -func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability credential resource schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`username`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the credential is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: "The Observability Instance ID the credential belongs to.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "description": schema.StringAttribute{ - Description: "A description of the credential.", - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "username": schema.StringAttribute{ - Description: "Credential username", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "password": schema.StringAttribute{ - Description: "Credential password", - Computed: true, - Sensitive: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - description := model.Description.ValueStringPointer() - - got, err := r.client.CreateCredentials(ctx, instanceId, projectId).CreateCredentialsPayload( - observability.CreateCredentialsPayload{ - Description: description, - }, - ).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(got.Credentials, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Observability credential created") -} - -func mapFields(r *observability.Credentials, model *Model) error { - if r == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - var userName string - if model.Username.ValueString() != "" { - userName = model.Username.ValueString() - } else if r.Username != nil { - userName = *r.Username - } else { - return fmt.Errorf("username id not present") - } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), model.InstanceId.ValueString(), userName, - ) - model.Username = types.StringPointerValue(r.Username) - model.Password = types.StringPointerValue(r.Password) - return nil -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userName := model.Username.ValueString() - _, err := r.client.GetCredentials(ctx, instanceId, projectId, userName).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading credential", - fmt.Sprintf("Credential with username %q or instance with ID %q does not exist in project %q.", userName, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Observability credential read") -} - -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userName := model.Username.ValueString() - _, err := r.client.DeleteCredentials(ctx, instanceId, projectId, userName).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Observability credential deleted") -} diff --git a/stackit/internal/services/observability/credential/resource_test.go b/stackit/internal/services/observability/credential/resource_test.go deleted file mode 100644 index 34ace309..00000000 --- a/stackit/internal/services/observability/credential/resource_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package observability - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *observability.Credentials - expected Model - isValid bool - }{ - { - "ok", - &observability.Credentials{ - Username: utils.Ptr("username"), - Password: utils.Ptr("password"), - }, - Model{ - Id: types.StringValue("pid,iid,username"), - ProjectId: types.StringValue("pid"), - InstanceId: types.StringValue("iid"), - Username: types.StringValue("username"), - Password: types.StringValue("password"), - }, - true, - }, - { - "response_nil_fail", - nil, - Model{}, - false, - }, - { - "response_fields_nil_fail", - &observability.Credentials{ - Password: nil, - Username: nil, - }, - Model{}, - false, - }, - { - "no_resource_id", - &observability.Credentials{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/observability/instance/datasource.go b/stackit/internal/services/observability/instance/datasource.go deleted file mode 100644 index 31b6cbd5..00000000 --- a/stackit/internal/services/observability/instance/datasource.go +++ /dev/null @@ -1,534 +0,0 @@ -package observability - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *observability.APIClient -} - -// Metadata returns the data source type name. -func (d *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_instance" -} - -func (d *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "Observability instance client configured") -} - -// Schema defines the schema for the data source. -func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability instance data source schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source. ID. It is structured as \"`project_id`,`instance_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the instance is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: "The Observability instance ID.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the Observability instance.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(300), - }, - }, - "plan_name": schema.StringAttribute{ - Description: "Specifies the Observability plan. E.g. `Observability-Monitoring-Medium-EU01`.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(200), - }, - }, - "plan_id": schema.StringAttribute{ - Description: "The Observability plan ID.", - Computed: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "parameters": schema.MapAttribute{ - Description: "Additional parameters.", - Computed: true, - ElementType: types.StringType, - }, - "dashboard_url": schema.StringAttribute{ - Description: "Specifies Observability instance dashboard URL.", - Computed: true, - }, - "is_updatable": schema.BoolAttribute{ - Description: "Specifies if the instance can be updated.", - Computed: true, - }, - "grafana_public_read_access": schema.BoolAttribute{ - Description: "If true, anyone can access Grafana dashboards without logging in.", - Computed: true, - }, - "grafana_url": schema.StringAttribute{ - Description: "Specifies Grafana URL.", - Computed: true, - }, - "grafana_initial_admin_user": schema.StringAttribute{ - Description: "Specifies an initial Grafana admin username.", - Computed: true, - }, - "grafana_initial_admin_password": schema.StringAttribute{ - Description: "Specifies an initial Grafana admin password.", - Computed: true, - Sensitive: true, - }, - "traces_retention_days": schema.Int64Attribute{ - Description: "Specifies for how many days the traces are kept. Default is set to `7`.", - Computed: true, - }, - "logs_retention_days": schema.Int64Attribute{ - Description: "Specifies for how many days the logs are kept. Default is set to `7`.", - Computed: true, - }, - "metrics_retention_days": schema.Int64Attribute{ - Description: "Specifies for how many days the raw metrics are kept. Default is set to `90`.", - Computed: true, - }, - "metrics_retention_days_5m_downsampling": schema.Int64Attribute{ - Description: "Specifies for how many days the 5m downsampled metrics are kept. must be less than the value of the general retention. Default is set to `90`.", - Computed: true, - }, - "metrics_retention_days_1h_downsampling": schema.Int64Attribute{ - Description: "Specifies for how many days the 1h downsampled metrics are kept. must be less than the value of the 5m downsampling retention. Default is set to `90`.", - Computed: true, - }, - "metrics_url": schema.StringAttribute{ - Description: "Specifies metrics URL.", - Computed: true, - }, - "metrics_push_url": schema.StringAttribute{ - Description: "Specifies URL for pushing metrics.", - Computed: true, - }, - "targets_url": schema.StringAttribute{ - Description: "Specifies Targets URL.", - Computed: true, - }, - "alerting_url": schema.StringAttribute{ - Description: "Specifies Alerting URL.", - Computed: true, - }, - "logs_url": schema.StringAttribute{ - Description: "Specifies Logs URL.", - Computed: true, - }, - "logs_push_url": schema.StringAttribute{ - Description: "Specifies URL for pushing logs.", - Computed: true, - }, - "jaeger_traces_url": schema.StringAttribute{ - Computed: true, - }, - "jaeger_ui_url": schema.StringAttribute{ - Computed: true, - }, - "otlp_traces_url": schema.StringAttribute{ - Computed: true, - }, - "zipkin_spans_url": schema.StringAttribute{ - Computed: true, - }, - "acl": schema.SetAttribute{ - Description: "The access control list for this instance. Each entry is an IP address range that is permitted to access, in CIDR notation.", - ElementType: types.StringType, - Computed: true, - }, - "alert_config": schema.SingleNestedAttribute{ - Description: "Alert configuration for the instance.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "receivers": schema.ListNestedAttribute{ - Description: "List of alert receivers.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "Name of the receiver.", - Computed: true, - }, - "email_configs": schema.ListNestedAttribute{ - Description: "List of email configurations.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "auth_identity": schema.StringAttribute{ - Description: "SMTP authentication information. Must be a valid email address", - Computed: true, - }, - "auth_password": schema.StringAttribute{ - Description: "SMTP authentication password.", - Computed: true, - Sensitive: true, - }, - "auth_username": schema.StringAttribute{ - Description: "SMTP authentication username.", - Computed: true, - }, - "from": schema.StringAttribute{ - Description: "The sender email address. Must be a valid email address", - Computed: true, - }, - "send_resolved": schema.BoolAttribute{ - Description: "Whether to notify about resolved alerts.", - Computed: true, - }, - "smart_host": schema.StringAttribute{ - Description: "The SMTP host through which emails are sent.", - Computed: true, - }, - "to": schema.StringAttribute{ - Description: "The email address to send notifications to. Must be a valid email address", - Computed: true, - }, - }, - }, - }, - "opsgenie_configs": schema.ListNestedAttribute{ - Description: "List of OpsGenie configurations.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "api_key": schema.StringAttribute{ - Description: "The API key for OpsGenie.", - Computed: true, - }, - "api_url": schema.StringAttribute{ - Description: "The host to send OpsGenie API requests to. Must be a valid URL", - Computed: true, - }, - "tags": schema.StringAttribute{ - Description: "Comma separated list of tags attached to the notifications.", - Computed: true, - }, - "priority": schema.StringAttribute{ - Description: "Priority of the alert. " + utils.FormatPossibleValues([]string{"P1", "P2", "P3", "P4", "P5"}...), - Computed: true, - }, - "send_resolved": schema.BoolAttribute{ - Description: "Whether to notify about resolved alerts.", - Computed: true, - }, - }, - }, - }, - "webhooks_configs": schema.ListNestedAttribute{ - Description: "List of Webhooks configurations.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "url": schema.StringAttribute{ - Description: "The endpoint to send HTTP POST requests to. Must be a valid URL", - Computed: true, - Sensitive: true, - }, - "ms_teams": schema.BoolAttribute{ - Description: "Microsoft Teams webhooks require special handling, set this to true if the webhook is for Microsoft Teams.", - Computed: true, - }, - "google_chat": schema.BoolAttribute{ - Description: "Google Chat webhooks require special handling, set this to true if the webhook is for Google Chat.", - Computed: true, - }, - "send_resolved": schema.BoolAttribute{ - Description: "Whether to notify about resolved alerts.", - Computed: true, - }, - }, - }, - }, - }, - }, - }, - "route": schema.SingleNestedAttribute{ - Description: "The route for the alert.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "group_by": schema.ListAttribute{ - Description: "The labels by which incoming alerts are grouped together. For example, multiple alerts coming in for cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels use the special value '...' as the sole label name, for example: group_by: ['...']. This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what you want, unless you have a very low alert volume or your upstream notification system performs its own grouping.", - Computed: true, - ElementType: types.StringType, - }, - "group_interval": schema.StringAttribute{ - Description: "How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent. (Usually ~5m or more.)", - Computed: true, - }, - "group_wait": schema.StringAttribute{ - Description: "How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.) .", - Computed: true, - }, - "receiver": schema.StringAttribute{ - Description: "The name of the receiver to route the alerts to.", - Computed: true, - }, - "repeat_interval": schema.StringAttribute{ - Description: "How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more).", - Computed: true, - }, - "routes": getDatasourceRouteNestedObject(), - }, - }, - "global": schema.SingleNestedAttribute{ - Description: "Global configuration for the alerts.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "opsgenie_api_key": schema.StringAttribute{ - Description: "The API key for OpsGenie.", - Computed: true, - Sensitive: true, - }, - "opsgenie_api_url": schema.StringAttribute{ - Description: "The host to send OpsGenie API requests to. Must be a valid URL", - Computed: true, - }, - "resolve_timeout": schema.StringAttribute{ - Description: "The default value used by alertmanager if the alert does not include EndsAt. After this time passes, it can declare the alert as resolved if it has not been updated. This has no impact on alerts from Prometheus, as they always include EndsAt.", - Computed: true, - }, - "smtp_auth_identity": schema.StringAttribute{ - Description: "SMTP authentication information. Must be a valid email address", - Computed: true, - }, - "smtp_auth_password": schema.StringAttribute{ - Description: "SMTP Auth using LOGIN and PLAIN.", - Computed: true, - Sensitive: true, - }, - "smtp_auth_username": schema.StringAttribute{ - Description: "SMTP Auth using CRAM-MD5, LOGIN and PLAIN. If empty, Alertmanager doesn't authenticate to the SMTP server.", - Computed: true, - }, - "smtp_from": schema.StringAttribute{ - Description: "The default SMTP From header field. Must be a valid email address", - Computed: true, - }, - "smtp_smart_host": schema.StringAttribute{ - Description: "The default SMTP smarthost used for sending emails, including port number. Port number usually is 25, or 587 for SMTP over TLS (sometimes referred to as STARTTLS).", - Computed: true, - }, - }, - }, - }, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - instanceResp, err := d.client.GetInstance(ctx, instanceId, projectId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - if instanceResp != nil && instanceResp.Status != nil && *instanceResp.Status == observability.GETINSTANCERESPONSESTATUS_DELETE_SUCCEEDED { - resp.State.RemoveResource(ctx) - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", "Instance was deleted successfully") - return - } - - aclListResp, err := d.client.ListACL(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to instance populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - plan, err := loadPlanId(ctx, *d.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - err = mapACLField(aclListResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the ACL: %v", err)) - return - } - - // Set state to fully populated data - diags = setACL(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // There are some plans which does not offer storage e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetLogsStorage() != 0 && plan.GetTracesStorage() != 0 { - metricsRetentionResp, err := d.client.GetMetricsStorageRetention(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get metrics retention: %v", err)) - return - } - // Map response body to schema - err = mapMetricsRetentionField(metricsRetentionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the metrics retention: %v", err)) - return - } - // Set state to fully populated data - diags := setMetricsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Handle Logs Retentions - logsRetentionResp, err := d.client.GetLogsConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get logs retention: %v", err)) - return - } - - err = mapLogsRetentionField(logsRetentionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the logs retention: %v", err)) - return - } - - diags = setLogsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Handle Traces Retentions - tracesRetentionResp, err := d.client.GetTracesConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get traces retention: %v", err)) - return - } - - err = mapTracesRetentionField(tracesRetentionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the traces retention: %v", err)) - return - } - - diags = setTracesRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // There are plans where no alert matchers and receivers are present e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetAlertMatchers() != 0 && plan.GetAlertReceivers() != 0 { - alertConfigResp, err := d.client.GetAlertConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get alert config: %v", err)) - return - } - // Map response body to schema - err = mapAlertConfigField(ctx, alertConfigResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API response for the alert config: %v", err)) - return - } - - // Set state to fully populated data - diags = setAlertConfig(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - tflog.Info(ctx, "Observability instance read") -} diff --git a/stackit/internal/services/observability/instance/resource.go b/stackit/internal/services/observability/instance/resource.go deleted file mode 100644 index 3db875f1..00000000 --- a/stackit/internal/services/observability/instance/resource.go +++ /dev/null @@ -1,2714 +0,0 @@ -package observability - -import ( - "context" - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "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/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Currently, due to incorrect types in the API, the maximum recursion level for child routes is set to 1. -// Once this is fixed, the value should be set to 10 and toRoutePayload needs to be adjusted, to support it. -const childRouteMaxRecursionLevel = 1 - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - InstanceId types.String `tfsdk:"instance_id"` - Name types.String `tfsdk:"name"` - PlanName types.String `tfsdk:"plan_name"` - PlanId types.String `tfsdk:"plan_id"` - Parameters types.Map `tfsdk:"parameters"` - DashboardURL types.String `tfsdk:"dashboard_url"` - IsUpdatable types.Bool `tfsdk:"is_updatable"` - GrafanaURL types.String `tfsdk:"grafana_url"` - GrafanaPublicReadAccess types.Bool `tfsdk:"grafana_public_read_access"` - GrafanaInitialAdminPassword types.String `tfsdk:"grafana_initial_admin_password"` - GrafanaInitialAdminUser types.String `tfsdk:"grafana_initial_admin_user"` - MetricsRetentionDays types.Int64 `tfsdk:"metrics_retention_days"` - MetricsRetentionDays5mDownsampling types.Int64 `tfsdk:"metrics_retention_days_5m_downsampling"` - MetricsRetentionDays1hDownsampling types.Int64 `tfsdk:"metrics_retention_days_1h_downsampling"` - MetricsURL types.String `tfsdk:"metrics_url"` - MetricsPushURL types.String `tfsdk:"metrics_push_url"` - TargetsURL types.String `tfsdk:"targets_url"` - AlertingURL types.String `tfsdk:"alerting_url"` - LogsRetentionDays types.Int64 `tfsdk:"logs_retention_days"` - TracesRetentionDays types.Int64 `tfsdk:"traces_retention_days"` - LogsURL types.String `tfsdk:"logs_url"` - LogsPushURL types.String `tfsdk:"logs_push_url"` - JaegerTracesURL types.String `tfsdk:"jaeger_traces_url"` - JaegerUIURL types.String `tfsdk:"jaeger_ui_url"` - OtlpTracesURL types.String `tfsdk:"otlp_traces_url"` - ZipkinSpansURL types.String `tfsdk:"zipkin_spans_url"` - ACL types.Set `tfsdk:"acl"` - AlertConfig types.Object `tfsdk:"alert_config"` -} - -// Struct corresponding to Model.AlertConfig -type alertConfigModel struct { - GlobalConfiguration types.Object `tfsdk:"global"` - Receivers types.List `tfsdk:"receivers"` - Route types.Object `tfsdk:"route"` -} - -var alertConfigTypes = map[string]attr.Type{ - "receivers": types.ListType{ElemType: types.ObjectType{AttrTypes: receiversTypes}}, - "route": types.ObjectType{AttrTypes: mainRouteTypes}, - "global": types.ObjectType{AttrTypes: globalConfigurationTypes}, -} - -// Struct corresponding to Model.AlertConfig.global -type globalConfigurationModel struct { - OpsgenieApiKey types.String `tfsdk:"opsgenie_api_key"` - OpsgenieApiUrl types.String `tfsdk:"opsgenie_api_url"` - ResolveTimeout types.String `tfsdk:"resolve_timeout"` - SmtpAuthIdentity types.String `tfsdk:"smtp_auth_identity"` - SmtpAuthPassword types.String `tfsdk:"smtp_auth_password"` - SmtpAuthUsername types.String `tfsdk:"smtp_auth_username"` - SmtpFrom types.String `tfsdk:"smtp_from"` - SmtpSmartHost types.String `tfsdk:"smtp_smart_host"` -} - -var globalConfigurationTypes = map[string]attr.Type{ - "opsgenie_api_key": types.StringType, - "opsgenie_api_url": types.StringType, - "resolve_timeout": types.StringType, - "smtp_auth_identity": types.StringType, - "smtp_auth_password": types.StringType, - "smtp_auth_username": types.StringType, - "smtp_from": types.StringType, - "smtp_smart_host": types.StringType, -} - -// Struct corresponding to Model.AlertConfig.route -type mainRouteModel struct { - GroupBy types.List `tfsdk:"group_by"` - GroupInterval types.String `tfsdk:"group_interval"` - GroupWait types.String `tfsdk:"group_wait"` - Receiver types.String `tfsdk:"receiver"` - RepeatInterval types.String `tfsdk:"repeat_interval"` - Routes types.List `tfsdk:"routes"` -} - -// Struct corresponding to Model.AlertConfig.route -// This is used to map the routes between the mainRouteModel and the last level of recursion of the routes field -type routeModelMiddle struct { - Continue types.Bool `tfsdk:"continue"` - GroupBy types.List `tfsdk:"group_by"` - GroupInterval types.String `tfsdk:"group_interval"` - GroupWait types.String `tfsdk:"group_wait"` - // Deprecated: Match is deprecated and will be removed after 10th March 2026. Use Matchers instead - Match types.Map `tfsdk:"match"` - // Deprecated: MatchRegex is deprecated and will be removed after 10th March 2026. Use Matchers instead - MatchRegex types.Map `tfsdk:"match_regex"` - Matchers types.List `tfsdk:"matchers"` - Receiver types.String `tfsdk:"receiver"` - RepeatInterval types.String `tfsdk:"repeat_interval"` - Routes types.List `tfsdk:"routes"` -} - -// Struct corresponding to Model.AlertConfig.route but without the recursive routes field -// This is used to map the last level of recursion of the routes field -type routeModelNoRoutes struct { - Continue types.Bool `tfsdk:"continue"` - GroupBy types.List `tfsdk:"group_by"` - GroupInterval types.String `tfsdk:"group_interval"` - GroupWait types.String `tfsdk:"group_wait"` - // Deprecated: Match is deprecated and will be removed after 10th March 2026. Use Matchers instead - Match types.Map `tfsdk:"match"` - // Deprecated: MatchRegex is deprecated and will be removed after 10th March 2026. Use Matchers instead - MatchRegex types.Map `tfsdk:"match_regex"` - Matchers types.List `tfsdk:"matchers"` - Receiver types.String `tfsdk:"receiver"` - RepeatInterval types.String `tfsdk:"repeat_interval"` -} - -var mainRouteTypes = map[string]attr.Type{ - "group_by": types.ListType{ElemType: types.StringType}, - "group_interval": types.StringType, - "group_wait": types.StringType, - "receiver": types.StringType, - "repeat_interval": types.StringType, - "routes": types.ListType{ElemType: getRouteListType()}, -} - -// Struct corresponding to Model.AlertConfig.receivers -type receiversModel struct { - Name types.String `tfsdk:"name"` - EmailConfigs types.List `tfsdk:"email_configs"` - OpsGenieConfigs types.List `tfsdk:"opsgenie_configs"` - WebHooksConfigs types.List `tfsdk:"webhooks_configs"` -} - -var receiversTypes = map[string]attr.Type{ - "name": types.StringType, - "email_configs": types.ListType{ElemType: types.ObjectType{AttrTypes: emailConfigsTypes}}, - "opsgenie_configs": types.ListType{ElemType: types.ObjectType{AttrTypes: opsgenieConfigsTypes}}, - "webhooks_configs": types.ListType{ElemType: types.ObjectType{AttrTypes: webHooksConfigsTypes}}, -} - -// Struct corresponding to Model.AlertConfig.receivers.emailConfigs -type emailConfigsModel struct { - AuthIdentity types.String `tfsdk:"auth_identity"` - AuthPassword types.String `tfsdk:"auth_password"` - AuthUsername types.String `tfsdk:"auth_username"` - From types.String `tfsdk:"from"` - SendResolved types.Bool `tfsdk:"send_resolved"` - Smarthost types.String `tfsdk:"smart_host"` - To types.String `tfsdk:"to"` -} - -var emailConfigsTypes = map[string]attr.Type{ - "auth_identity": types.StringType, - "auth_password": types.StringType, - "auth_username": types.StringType, - "from": types.StringType, - "send_resolved": types.BoolType, - "smart_host": types.StringType, - "to": types.StringType, -} - -// Struct corresponding to Model.AlertConfig.receivers.opsGenieConfigs -type opsgenieConfigsModel struct { - ApiKey types.String `tfsdk:"api_key"` - ApiUrl types.String `tfsdk:"api_url"` - Tags types.String `tfsdk:"tags"` - Priority types.String `tfsdk:"priority"` - SendResolved types.Bool `tfsdk:"send_resolved"` -} - -var opsgenieConfigsTypes = map[string]attr.Type{ - "api_key": types.StringType, - "api_url": types.StringType, - "tags": types.StringType, - "priority": types.StringType, - "send_resolved": types.BoolType, -} - -// Struct corresponding to Model.AlertConfig.receivers.webHooksConfigs -type webHooksConfigsModel struct { - Url types.String `tfsdk:"url"` - MsTeams types.Bool `tfsdk:"ms_teams"` - GoogleChat types.Bool `tfsdk:"google_chat"` - SendResolved types.Bool `tfsdk:"send_resolved"` -} - -var webHooksConfigsTypes = map[string]attr.Type{ - "url": types.StringType, - "ms_teams": types.BoolType, - "google_chat": types.BoolType, - "send_resolved": types.BoolType, -} - -var routeDescriptions = map[string]string{ - "continue": "Whether an alert should continue matching subsequent sibling nodes.", - "group_by": "The labels by which incoming alerts are grouped together. For example, multiple alerts coming in for cluster=A and alertname=LatencyHigh would be batched into a single group. To aggregate by all possible labels use the special value '...' as the sole label name, for example: group_by: ['...']. This effectively disables aggregation entirely, passing through all alerts as-is. This is unlikely to be what you want, unless you have a very low alert volume or your upstream notification system performs its own grouping.", - "group_interval": "How long to wait before sending a notification about new alerts that are added to a group of alerts for which an initial notification has already been sent. (Usually ~5m or more.)", - "group_wait": "How long to initially wait to send a notification for a group of alerts. Allows to wait for an inhibiting alert to arrive or collect more initial alerts for the same group. (Usually ~0s to few minutes.)", - "match": "A set of equality matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead", - "match_regex": "A set of regex-matchers an alert has to fulfill to match the node. This field is deprecated and will be removed after 10th March 2026, use `matchers` in the `routes` instead", - "matchers": "A list of matchers that an alert has to fulfill to match the node. A matcher is a string with a syntax inspired by PromQL and OpenMetrics.", - "receiver": "The name of the receiver to route the alerts to.", - "repeat_interval": "How long to wait before sending a notification again if it has already been sent successfully for an alert. (Usually ~3h or more).", - "routes": "List of child routes.", -} - -// getRouteListType is a helper function to return the route list attribute type. -func getRouteListType() types.ObjectType { - return getRouteListTypeAux(1, childRouteMaxRecursionLevel) -} - -// getRouteListTypeAux returns the type of the route list attribute with the given level of child routes recursion. -// The level is used to determine the current depth of the nested object. -// The limit is used to determine the maximum depth of the nested object. -// The level should be lower or equal to the limit, if higher, the function will produce a stack overflow. -func getRouteListTypeAux(level, limit int) types.ObjectType { - attributeTypes := map[string]attr.Type{ - "continue": types.BoolType, - "group_by": types.ListType{ElemType: types.StringType}, - "group_interval": types.StringType, - "group_wait": types.StringType, - "match": types.MapType{ElemType: types.StringType}, - "match_regex": types.MapType{ElemType: types.StringType}, - "matchers": types.ListType{ElemType: types.StringType}, - "receiver": types.StringType, - "repeat_interval": types.StringType, - } - - if level != limit { - attributeTypes["routes"] = types.ListType{ElemType: getRouteListTypeAux(level+1, limit)} - } - - return types.ObjectType{AttrTypes: attributeTypes} -} - -func getRouteNestedObject() schema.ListNestedAttribute { - return getRouteNestedObjectAux(false, 1, childRouteMaxRecursionLevel) -} - -func getDatasourceRouteNestedObject() schema.ListNestedAttribute { - return getRouteNestedObjectAux(true, 1, childRouteMaxRecursionLevel) -} - -// getRouteNestedObjectAux returns the nested object for the route attribute with the given level of child routes recursion. -// The isDatasource is used to determine if the route is used in a datasource schema or not. If it is a datasource, all fields are computed. -// The level is used to determine the current depth of the nested object. -// The limit is used to determine the maximum depth of the nested object. -// The level should be lower or equal to the limit, if higher, the function will produce a stack overflow. -func getRouteNestedObjectAux(isDatasource bool, level, limit int) schema.ListNestedAttribute { - attributesMap := map[string]schema.Attribute{ - "continue": schema.BoolAttribute{ - Description: routeDescriptions["continue"], - Optional: !isDatasource, - Computed: isDatasource, - }, - "group_by": schema.ListAttribute{ - Description: routeDescriptions["group_by"], - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "group_interval": schema.StringAttribute{ - Description: routeDescriptions["group_interval"], - Optional: !isDatasource, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "group_wait": schema.StringAttribute{ - Description: routeDescriptions["group_wait"], - Optional: !isDatasource, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "match": schema.MapAttribute{ - Description: routeDescriptions["match"], - DeprecationMessage: "Use `matchers` in the `routes` instead.", - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "match_regex": schema.MapAttribute{ - Description: routeDescriptions["match_regex"], - DeprecationMessage: "Use `matchers` in the `routes` instead.", - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "matchers": schema.ListAttribute{ - Description: routeDescriptions["matchers"], - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "receiver": schema.StringAttribute{ - Description: routeDescriptions["receiver"], - Required: !isDatasource, - Computed: isDatasource, - }, - "repeat_interval": schema.StringAttribute{ - Description: routeDescriptions["repeat_interval"], - Optional: !isDatasource, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - } - - if level != limit { - attributesMap["routes"] = getRouteNestedObjectAux(isDatasource, level+1, limit) - } - - return schema.ListNestedAttribute{ - Description: routeDescriptions["routes"], - Optional: !isDatasource, - Computed: isDatasource, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: attributesMap, - }, - } -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *observability.APIClient -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Observability instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability instance resource schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the instance is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: "The Observability instance ID.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The name of the Observability instance.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(200), - }, - }, - "plan_name": schema.StringAttribute{ - Description: "Specifies the Observability plan. E.g. `Observability-Monitoring-Medium-EU01`.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(200), - }, - }, - "plan_id": schema.StringAttribute{ - Description: "The Observability plan ID.", - Computed: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "parameters": schema.MapAttribute{ - Description: "Additional parameters.", - Optional: true, - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.Map{ - mapplanmodifier.UseStateForUnknown(), - }, - }, - "dashboard_url": schema.StringAttribute{ - Description: "Specifies Observability instance dashboard URL.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "is_updatable": schema.BoolAttribute{ - Description: "Specifies if the instance can be updated.", - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "grafana_public_read_access": schema.BoolAttribute{ - Description: "If true, anyone can access Grafana dashboards without logging in.", - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "grafana_url": schema.StringAttribute{ - Description: "Specifies Grafana URL.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "grafana_initial_admin_user": schema.StringAttribute{ - Description: "Specifies an initial Grafana admin username.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "grafana_initial_admin_password": schema.StringAttribute{ - Description: "Specifies an initial Grafana admin password.", - Computed: true, - Sensitive: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "traces_retention_days": schema.Int64Attribute{ - Description: "Specifies for how many days the traces are kept. Default is set to `7`.", - Optional: true, - Computed: true, - }, - "logs_retention_days": schema.Int64Attribute{ - Description: "Specifies for how many days the logs are kept. Default is set to `7`.", - Optional: true, - Computed: true, - }, - "metrics_retention_days": schema.Int64Attribute{ - Description: "Specifies for how many days the raw metrics are kept. Default is set to `90`.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "metrics_retention_days_5m_downsampling": schema.Int64Attribute{ - Description: "Specifies for how many days the 5m downsampled metrics are kept. must be less than the value of the general retention. Default is set to `90`.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "metrics_retention_days_1h_downsampling": schema.Int64Attribute{ - Description: "Specifies for how many days the 1h downsampled metrics are kept. must be less than the value of the 5m downsampling retention. Default is set to `90`.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "metrics_url": schema.StringAttribute{ - Description: "Specifies metrics URL.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "metrics_push_url": schema.StringAttribute{ - Description: "Specifies URL for pushing metrics.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "targets_url": schema.StringAttribute{ - Description: "Specifies Targets URL.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "alerting_url": schema.StringAttribute{ - Description: "Specifies Alerting URL.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "logs_url": schema.StringAttribute{ - Description: "Specifies Logs URL.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "logs_push_url": schema.StringAttribute{ - Description: "Specifies URL for pushing logs.", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "jaeger_traces_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "jaeger_ui_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "otlp_traces_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "zipkin_spans_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "acl": schema.SetAttribute{ - Description: "The access control list for this instance. Each entry is an IP address range that is permitted to access, in CIDR notation.", - ElementType: types.StringType, - Optional: true, - Validators: []validator.Set{ - setvalidator.ValueStringsAre( - validate.CIDR(), - ), - }, - }, - "alert_config": schema.SingleNestedAttribute{ - Description: "Alert configuration for the instance.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "receivers": schema.ListNestedAttribute{ - Description: "List of alert receivers.", - Required: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "Name of the receiver.", - Required: true, - }, - "email_configs": schema.ListNestedAttribute{ - Description: "List of email configurations.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "auth_identity": schema.StringAttribute{ - Description: "SMTP authentication information. Must be a valid email address", - Optional: true, - }, - "auth_password": schema.StringAttribute{ - Description: "SMTP authentication password.", - Optional: true, - Sensitive: true, - }, - "auth_username": schema.StringAttribute{ - Description: "SMTP authentication username.", - Optional: true, - }, - "from": schema.StringAttribute{ - Description: "The sender email address. Must be a valid email address", - Optional: true, - }, - "send_resolved": schema.BoolAttribute{ - Description: "Whether to notify about resolved alerts.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - }, - "smart_host": schema.StringAttribute{ - Description: "The SMTP host through which emails are sent.", - Optional: true, - }, - "to": schema.StringAttribute{ - Description: "The email address to send notifications to. Must be a valid email address", - Optional: true, - }, - }, - }, - }, - "opsgenie_configs": schema.ListNestedAttribute{ - Description: "List of OpsGenie configurations.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "api_key": schema.StringAttribute{ - Description: "The API key for OpsGenie.", - Optional: true, - }, - "api_url": schema.StringAttribute{ - Description: "The host to send OpsGenie API requests to. Must be a valid URL", - Optional: true, - }, - "tags": schema.StringAttribute{ - Description: "Comma separated list of tags attached to the notifications.", - Optional: true, - }, - "priority": schema.StringAttribute{ - Description: "Priority of the alert. " + utils.FormatPossibleValues("P1", "P2", "P3", "P4", "P5"), - Optional: true, - }, - "send_resolved": schema.BoolAttribute{ - Description: "Whether to notify about resolved alerts.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - }, - }, - }, - }, - "webhooks_configs": schema.ListNestedAttribute{ - Description: "List of Webhooks configurations.", - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Validators: []validator.Object{ - WebhookConfigMutuallyExclusive(), - }, - Attributes: map[string]schema.Attribute{ - "url": schema.StringAttribute{ - Description: "The endpoint to send HTTP POST requests to. Must be a valid URL", - Optional: true, - Sensitive: true, - }, - "ms_teams": schema.BoolAttribute{ - Description: "Microsoft Teams webhooks require special handling, set this to true if the webhook is for Microsoft Teams.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - "google_chat": schema.BoolAttribute{ - Description: "Google Chat webhooks require special handling, set this to true if the webhook is for Google Chat.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(false), - }, - "send_resolved": schema.BoolAttribute{ - Description: "Whether to notify about resolved alerts.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - }, - }, - }, - }, - }, - }, - }, - "route": schema.SingleNestedAttribute{ - Description: "Route configuration for the alerts.", - Required: true, - Attributes: map[string]schema.Attribute{ - "group_by": schema.ListAttribute{ - Description: routeDescriptions["group_by"], - Optional: true, - ElementType: types.StringType, - }, - "group_interval": schema.StringAttribute{ - Description: routeDescriptions["group_interval"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "group_wait": schema.StringAttribute{ - Description: routeDescriptions["group_wait"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "receiver": schema.StringAttribute{ - Description: routeDescriptions["receiver"], - Required: true, - }, - "repeat_interval": schema.StringAttribute{ - Description: routeDescriptions["repeat_interval"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "routes": getRouteNestedObject(), - }, - }, - "global": schema.SingleNestedAttribute{ - Description: "Global configuration for the alerts. If nothing passed the default argus config will be used. It is only possible to update the entire global part, not individual attributes.", - Optional: true, - Computed: true, - Attributes: map[string]schema.Attribute{ - "opsgenie_api_key": schema.StringAttribute{ - Description: "The API key for OpsGenie.", - Optional: true, - Sensitive: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "opsgenie_api_url": schema.StringAttribute{ - Description: "The host to send OpsGenie API requests to. Must be a valid URL", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "resolve_timeout": schema.StringAttribute{ - Description: "The default value used by alertmanager if the alert does not include EndsAt. After this time passes, it can declare the alert as resolved if it has not been updated. This has no impact on alerts from Prometheus, as they always include EndsAt.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "smtp_auth_identity": schema.StringAttribute{ - Description: "SMTP authentication information. Must be a valid email address", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "smtp_auth_password": schema.StringAttribute{ - Description: "SMTP Auth using LOGIN and PLAIN.", - Optional: true, - Sensitive: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "smtp_auth_username": schema.StringAttribute{ - Description: "SMTP Auth using CRAM-MD5, LOGIN and PLAIN. If empty, Alertmanager doesn't authenticate to the SMTP server.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "smtp_from": schema.StringAttribute{ - Description: "The default SMTP From header field. Must be a valid email address", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "smtp_smart_host": schema.StringAttribute{ - Description: "The default SMTP smarthost used for sending emails, including port number in format `host:port` (eg. `smtp.example.com:587`). Port number usually is 25, or 587 for SMTP over TLS (sometimes referred to as STARTTLS).", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - }, - }, - }, - }, - } -} - -// ModifyPlan will be called in the Plan phase. -// It will check if the plan contains a change that requires replacement. If yes, it will show an error to the user. -// Since there are observabiltiy plans which do not support specific configurations the request needs to be aborted with an error. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - // If the plan is empty we are deleting the resource - if req.Plan.Raw.IsNull() { - return - } - - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - plan, err := loadPlanId(ctx, *r.client, &configModel) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error validating plan", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Plan does not support alert config - if plan.GetAlertMatchers() == 0 && plan.GetAlertReceivers() == 0 { - // If an alert config was set, return an error to the user - if !(utils.IsUndefined(configModel.AlertConfig)) { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error validating plan", fmt.Sprintf("Plan (%s) does not support configuring an alert config. Remove this from your config or use a different plan.", *plan.Name)) - } - } - - // Plan does not support log storage and trace storage - if plan.GetLogsStorage() == 0 && plan.GetTracesStorage() == 0 { - logsRetentionDays := conversion.Int64ValueToPointer(configModel.LogsRetentionDays) - tracesRetentionDays := conversion.Int64ValueToPointer(configModel.TracesRetentionDays) - metricsRetentionDays := conversion.Int64ValueToPointer(configModel.MetricsRetentionDays) - metricsRetentionDays5mDownsampling := conversion.Int64ValueToPointer(configModel.MetricsRetentionDays5mDownsampling) - metricsRetentionDays1hDownsampling := conversion.Int64ValueToPointer(configModel.MetricsRetentionDays1hDownsampling) - // If logs retention days are set, return an error to the user - if logsRetentionDays != nil { - resp.Diagnostics.AddAttributeError(path.Root("logs_retention_days"), "Error validating plan", fmt.Sprintf("Plan (%s) does not support configuring logs retention days. Remove this from your config or use a different plan.", *plan.Name)) - } - // If traces retention days are set, return an error to the user - if tracesRetentionDays != nil { - resp.Diagnostics.AddAttributeError(path.Root("traces_retention_days"), "Error validating plan", fmt.Sprintf("Plan (%s) does not support configuring trace retention days. Remove this from your config or use a different plan.", *plan.Name)) - } - // If any of the metrics retention days are set, return an error to the user - if metricsRetentionDays != nil || metricsRetentionDays5mDownsampling != nil || metricsRetentionDays1hDownsampling != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error validating plan", fmt.Sprintf("Plan (%s) does not support configuring metrics retention days. Remove this from your config or use a different plan.", *plan.Name)) - } - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - acl := []string{} - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - alertConfig := alertConfigModel{} - if !(model.AlertConfig.IsNull() || model.AlertConfig.IsUnknown()) { - diags = model.AlertConfig.As(ctx, &alertConfig, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - plan, err := loadPlanId(ctx, *r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - // Generate API request body from model - createPayload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*createPayload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := createResp.InstanceId - ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, *instanceId, projectId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to instance populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Create ACL - err = updateACL(ctx, projectId, *instanceId, acl, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating ACL: %v", err)) - return - } - aclList, err := r.client.ListACL(ctx, *instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) - return - } - - // Map response body to schema - err = mapACLField(aclList, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API response for the ACL: %v", err)) - return - } - - // Set state to fully populated data - diags = setACL(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // There are some plans which does not offer storage e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetLogsStorage() != 0 && plan.GetTracesStorage() != 0 { - err := r.getMetricsRetention(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("%v", err)) - } - - // Set state to fully populated data - diags = setMetricsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err = r.getLogsRetention(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("%v", err)) - } - - diags = setLogsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err = r.getTracesRetention(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("%v", err)) - } - - diags = setTracesRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } else { - // Set metric retention days to zero - diags = setMetricsRetentionsZero(ctx, &resp.State) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - // Set logs retention days to zero - diags = setLogsRetentionsZero(ctx, &resp.State) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - // Set traces retention days to zero - diags = setTracesRetentionsZero(ctx, &resp.State) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // There are plans where no alert matchers and receivers are present e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetAlertMatchers() != 0 && plan.GetAlertReceivers() != 0 { - err := r.getAlertConfigs(ctx, &alertConfig, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("%v", err)) - } - - // Set state to fully populated data - diags = setAlertConfig(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - tflog.Info(ctx, "Observability instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, instanceId, projectId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if instanceResp != nil && instanceResp.Status != nil && *instanceResp.Status == observability.GETINSTANCERESPONSESTATUS_DELETE_SUCCEEDED { - resp.State.RemoveResource(ctx) - return - } - - // Map response body to schema - err = mapFields(ctx, instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - plan, err := loadPlanId(ctx, *r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - aclListResp, err := r.client.ListACL(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACL data: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Map response body to schema - err = mapACLField(aclListResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the ACL: %v", err)) - return - } - - // Set state to fully populated data - diags = setACL(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // There are some plans which does not offer storage e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetLogsStorage() != 0 && plan.GetTracesStorage() != 0 { - metricsRetentionResp, err := r.client.GetMetricsStorageRetention(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get metrics retention: %v", err)) - return - } - // Map response body to schema - err = mapMetricsRetentionField(metricsRetentionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the metrics retention: %v", err)) - return - } - // Set state to fully populated data - diags = setMetricsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - logsRetentionResp, err := r.client.GetLogsConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get logs retention: %v", err)) - return - } - // Map response body to schema - err = mapLogsRetentionField(logsRetentionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the logs retention: %v", err)) - return - } - // Set state to fully populated data - diags = setLogsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tracesRetentionResp, err := r.client.GetTracesConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get logs retention: %v", err)) - return - } - // Map response body to schema - err = mapTracesRetentionField(tracesRetentionResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API response for the logs retention: %v", err)) - return - } - // Set state to fully populated data - diags = setTracesRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // There are plans where no alert matchers and receivers are present e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetAlertMatchers() != 0 && plan.GetAlertReceivers() != 0 { - alertConfigResp, err := r.client.GetAlertConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API to get alert config: %v", err)) - return - } - // Map response body to schema - err = mapAlertConfigField(ctx, alertConfigResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API response for the alert config: %v", err)) - return - } - - // Set state to fully populated data - diags = setAlertConfig(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - tflog.Info(ctx, "Observability instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - - acl := []string{} - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - alertConfig := alertConfigModel{} - if !(model.AlertConfig.IsNull() || model.AlertConfig.IsUnknown()) { - diags = model.AlertConfig.As(ctx, &alertConfig, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - plan, err := loadPlanId(ctx, *r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - var previousState Model - diags = req.State.Get(ctx, &previousState) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - previousStatePayload, err := toUpdatePayload(&previousState) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating previous state payload: %v", err)) - return - } - var instance *observability.GetInstanceResponse - // This check is required, because when values should be updated, that needs to be updated via a different endpoint, the waiter will run into a timeout - if !cmp.Equal(previousStatePayload, payload) { - // Update existing instance - _, err = r.client.UpdateInstance(ctx, instanceId, projectId).UpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instance, err = wait.UpdateInstanceWaitHandler(ctx, r.client, instanceId, projectId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - } else { - instance, err = r.client.GetInstanceExecute(ctx, instanceId, projectId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance read: %v", err)) - return - } - } - - err = mapFields(ctx, instance, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Update ACL - err = updateACL(ctx, projectId, instanceId, acl, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Updating ACL: %v", err)) - return - } - aclList, err := r.client.ListACL(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) - return - } - - // Map response body to schema - err = mapACLField(aclList, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API response for the ACL: %v", err)) - return - } - - // Set state to ACL populated data - resp.Diagnostics.Append(setACL(ctx, &resp.State, &model)...) - if resp.Diagnostics.HasError() { - return - } - - // There are some plans which does not offer storage e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetLogsStorage() != 0 && plan.GetTracesStorage() != 0 { - err := r.getMetricsRetention(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("%v", err)) - } - - // Set state to fully populated data - diags = setMetricsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err = r.getLogsRetention(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("%v", err)) - } - - diags = setLogsRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err = r.getTracesRetention(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("%v", err)) - } - - diags = setTracesRetentions(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } else { - // Set metric retention days to zero - diags = setMetricsRetentionsZero(ctx, &resp.State) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - diags = setLogsRetentionsZero(ctx, &resp.State) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - diags = setTracesRetentionsZero(ctx, &resp.State) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // There are plans where no alert matchers and receivers are present e.g. like Observability-Metrics-Endpoint-100k-EU01 - if plan.GetAlertMatchers() != 0 && plan.GetAlertReceivers() != 0 { - err := r.getAlertConfigs(ctx, &alertConfig, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("%v", err)) - } - - // Set state to fully populated data - diags = setAlertConfig(ctx, &resp.State, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - tflog.Info(ctx, "Observability instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - - // Delete existing instance - _, err := r.client.DeleteInstance(ctx, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, instanceId, projectId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Observability instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "Observability instance state imported") -} - -func mapFields(ctx context.Context, r *observability.GetInstanceResponse, model *Model) error { - if r == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if r.Id != nil { - instanceId = *r.Id - } else { - return fmt.Errorf("instance id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) - model.InstanceId = types.StringValue(instanceId) - model.PlanName = types.StringPointerValue(r.PlanName) - model.PlanId = types.StringPointerValue(r.PlanId) - model.Name = types.StringPointerValue(r.Name) - - ps := r.Parameters - if ps == nil { - model.Parameters = types.MapNull(types.StringType) - } else { - params := make(map[string]attr.Value, len(*ps)) - for k, v := range *ps { - params[k] = types.StringValue(v) - } - res, diags := types.MapValueFrom(ctx, types.StringType, params) - if diags.HasError() { - return fmt.Errorf("parameter mapping %s", diags.Errors()) - } - model.Parameters = res - } - - model.IsUpdatable = types.BoolPointerValue(r.IsUpdatable) - model.DashboardURL = types.StringPointerValue(r.DashboardUrl) - if r.Instance != nil { - i := *r.Instance - model.GrafanaURL = types.StringPointerValue(i.GrafanaUrl) - model.GrafanaPublicReadAccess = types.BoolPointerValue(i.GrafanaPublicReadAccess) - model.GrafanaInitialAdminPassword = types.StringPointerValue(i.GrafanaAdminPassword) - model.GrafanaInitialAdminUser = types.StringPointerValue(i.GrafanaAdminUser) - model.MetricsURL = types.StringPointerValue(i.MetricsUrl) - model.MetricsPushURL = types.StringPointerValue(i.PushMetricsUrl) - model.TargetsURL = types.StringPointerValue(i.TargetsUrl) - model.AlertingURL = types.StringPointerValue(i.AlertingUrl) - model.LogsURL = types.StringPointerValue(i.LogsUrl) - model.LogsPushURL = types.StringPointerValue(i.LogsPushUrl) - model.JaegerTracesURL = types.StringPointerValue(i.JaegerTracesUrl) - model.JaegerUIURL = types.StringPointerValue(i.JaegerUiUrl) - model.OtlpTracesURL = types.StringPointerValue(i.OtlpTracesUrl) - model.ZipkinSpansURL = types.StringPointerValue(i.ZipkinSpansUrl) - } - - return nil -} - -func mapACLField(aclList *observability.ListACLResponse, model *Model) error { - if aclList == nil { - return fmt.Errorf("mapping ACL: nil API response") - } - - if aclList.Acl == nil || len(*aclList.Acl) == 0 { - if !(model.ACL.IsNull() || model.ACL.IsUnknown() || model.ACL.Equal(types.SetValueMust(types.StringType, []attr.Value{}))) { - model.ACL = types.SetNull(types.StringType) - } - return nil - } - - acl := []attr.Value{} - for _, cidr := range *aclList.Acl { - acl = append(acl, types.StringValue(cidr)) - } - aclTF, diags := types.SetValue(types.StringType, acl) - if diags.HasError() { - return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) - } - model.ACL = aclTF - return nil -} - -func mapLogsRetentionField(r *observability.LogsConfigResponse, model *Model) error { - if r == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if r.Config == nil { - return fmt.Errorf("logs retention config is nil") - } - - if r.Config.Retention == nil { - return fmt.Errorf("logs retention days is nil") - } - - stripedLogsRetentionHours := strings.TrimSuffix(*r.Config.Retention, "h") - logsRetentionHours, err := strconv.ParseInt(stripedLogsRetentionHours, 10, 64) - if err != nil { - return fmt.Errorf("parsing logs retention hours: %w", err) - } - model.LogsRetentionDays = types.Int64Value(logsRetentionHours / 24) - return nil -} - -func mapTracesRetentionField(r *observability.TracesConfigResponse, model *Model) error { - if r == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if r.Config == nil { - return fmt.Errorf("traces retention config is nil") - } - - if r.Config.Retention == nil { - return fmt.Errorf("traces retention days is nil") - } - - stripedTracesRetentionHours := strings.TrimSuffix(*r.Config.Retention, "h") - tracesRetentionHours, err := strconv.ParseInt(stripedTracesRetentionHours, 10, 64) - if err != nil { - return fmt.Errorf("parsing traces retention hours: %w", err) - } - model.TracesRetentionDays = types.Int64Value(tracesRetentionHours / 24) - return nil -} - -func mapMetricsRetentionField(r *observability.GetMetricsStorageRetentionResponse, model *Model) error { - if r == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if r.MetricsRetentionTimeRaw == nil || r.MetricsRetentionTime5m == nil || r.MetricsRetentionTime1h == nil { - return fmt.Errorf("metrics retention time is nil") - } - - stripedMetricsRetentionDays := strings.TrimSuffix(*r.MetricsRetentionTimeRaw, "d") - metricsRetentionDays, err := strconv.ParseInt(stripedMetricsRetentionDays, 10, 64) - if err != nil { - return fmt.Errorf("parsing metrics retention days: %w", err) - } - model.MetricsRetentionDays = types.Int64Value(metricsRetentionDays) - - stripedMetricsRetentionDays5m := strings.TrimSuffix(*r.MetricsRetentionTime5m, "d") - metricsRetentionDays5m, err := strconv.ParseInt(stripedMetricsRetentionDays5m, 10, 64) - if err != nil { - return fmt.Errorf("parsing metrics retention days 5m: %w", err) - } - model.MetricsRetentionDays5mDownsampling = types.Int64Value(metricsRetentionDays5m) - - stripedMetricsRetentionDays1h := strings.TrimSuffix(*r.MetricsRetentionTime1h, "d") - metricsRetentionDays1h, err := strconv.ParseInt(stripedMetricsRetentionDays1h, 10, 64) - if err != nil { - return fmt.Errorf("parsing metrics retention days 1h: %w", err) - } - model.MetricsRetentionDays1hDownsampling = types.Int64Value(metricsRetentionDays1h) - - return nil -} - -func mapAlertConfigField(ctx context.Context, resp *observability.GetAlertConfigsResponse, model *Model) error { - if resp == nil || resp.Data == nil { - model.AlertConfig = types.ObjectNull(alertConfigTypes) - return nil - } - - if model == nil { - return fmt.Errorf("nil model") - } - - var alertConfigTF *alertConfigModel - if !(model.AlertConfig.IsNull() || model.AlertConfig.IsUnknown()) { - alertConfigTF = &alertConfigModel{} - diags := model.AlertConfig.As(ctx, &alertConfigTF, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return fmt.Errorf("mapping alert config: %w", core.DiagsToError(diags)) - } - } - - respReceivers := resp.Data.Receivers - respRoute := resp.Data.Route - respGlobalConfigs := resp.Data.Global - - receiversList, err := mapReceiversToAttributes(ctx, respReceivers) - if err != nil { - return fmt.Errorf("mapping alert config receivers: %w", err) - } - - route, err := mapRouteToAttributes(ctx, respRoute) - if err != nil { - return fmt.Errorf("mapping alert config route: %w", err) - } - - var globalConfigModel *globalConfigurationModel - if alertConfigTF != nil && !alertConfigTF.GlobalConfiguration.IsNull() && !alertConfigTF.GlobalConfiguration.IsUnknown() { - globalConfigModel = &globalConfigurationModel{} - diags := alertConfigTF.GlobalConfiguration.As(ctx, globalConfigModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return fmt.Errorf("mapping alert config: %w", core.DiagsToError(diags)) - } - } - - globalConfig, err := mapGlobalConfigToAttributes(respGlobalConfigs, globalConfigModel) - if err != nil { - return fmt.Errorf("mapping alert config global config: %w", err) - } - - alertConfig, diags := types.ObjectValue(alertConfigTypes, map[string]attr.Value{ - "receivers": receiversList, - "route": route, - "global": globalConfig, - }) - if diags.HasError() { - return fmt.Errorf("converting alert config to TF type: %w", core.DiagsToError(diags)) - } - - // Check if the alert config is equal to the mock alert config - // This is done because the Alert Config cannot be removed from the instance, but can be unset by the user in the Terraform configuration - // If the alert config is equal to the mock alert config, we will map the Alert Config to an empty object in the Terraform state - // This is done to avoid inconsistent applies or non-empty plans after applying - mockAlertConfig, err := getMockAlertConfig(ctx) - if err != nil { - return fmt.Errorf("getting mock alert config: %w", err) - } - modelMockAlertConfig, diags := types.ObjectValueFrom(ctx, alertConfigTypes, mockAlertConfig) - if diags.HasError() { - return fmt.Errorf("converting mock alert config to TF type: %w", core.DiagsToError(diags)) - } - if alertConfig.Equal(modelMockAlertConfig) { - alertConfig = types.ObjectNull(alertConfigTypes) - } - - model.AlertConfig = alertConfig - return nil -} - -// getMockAlertConfig returns a default alert config to be set in the instance if the alert config is unset in the Terraform configuration -// -// This is done because the Alert Config cannot be removed from the instance, but can be unset by the user in the Terraform configuration. -// So, we set the Alert Config in the instance to our mock configuration and -// map the Alert Config to an empty object in the Terraform state if it matches the mock alert config -func getMockAlertConfig(ctx context.Context) (alertConfigModel, error) { - mockEmailConfig, diags := types.ObjectValue(emailConfigsTypes, map[string]attr.Value{ - "to": types.StringValue("123@gmail.com"), - "smart_host": types.StringValue("smtp.gmail.com:587"), - "send_resolved": types.BoolValue(false), - "from": types.StringValue("xxxx@gmail.com"), - "auth_username": types.StringValue("xxxx@gmail.com"), - "auth_password": types.StringValue("xxxxxxxxx"), - "auth_identity": types.StringValue("xxxx@gmail.com"), - }) - if diags.HasError() { - return alertConfigModel{}, fmt.Errorf("mapping email config: %w", core.DiagsToError(diags)) - } - - mockEmailConfigs, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: emailConfigsTypes}, []attr.Value{ - mockEmailConfig, - }) - if diags.HasError() { - return alertConfigModel{}, fmt.Errorf("mapping email configs: %w", core.DiagsToError(diags)) - } - - mockReceiver, diags := types.ObjectValue(receiversTypes, map[string]attr.Value{ - "name": types.StringValue("email-me"), - "email_configs": mockEmailConfigs, - "opsgenie_configs": types.ListNull(types.ObjectType{AttrTypes: opsgenieConfigsTypes}), - "webhooks_configs": types.ListNull(types.ObjectType{AttrTypes: webHooksConfigsTypes}), - }) - if diags.HasError() { - return alertConfigModel{}, fmt.Errorf("mapping receiver: %w", core.DiagsToError(diags)) - } - - mockReceivers, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - mockReceiver, - }) - if diags.HasError() { - return alertConfigModel{}, fmt.Errorf("mapping receivers: %w", core.DiagsToError(diags)) - } - - mockGroupByList, diags := types.ListValueFrom(ctx, types.StringType, []attr.Value{ - types.StringValue("job"), - }) - if diags.HasError() { - return alertConfigModel{}, fmt.Errorf("mapping group by list: %w", core.DiagsToError(diags)) - } - - mockRoute, diags := types.ObjectValue(mainRouteTypes, map[string]attr.Value{ - "receiver": types.StringValue("email-me"), - "group_by": mockGroupByList, - "group_wait": types.StringValue("30s"), - "group_interval": types.StringValue("5m"), - "repeat_interval": types.StringValue("4h"), - "routes": types.ListNull(getRouteListType()), - }) - if diags.HasError() { - return alertConfigModel{}, fmt.Errorf("mapping route: %w", core.DiagsToError(diags)) - } - - mockGlobalConfig, diags := types.ObjectValue(globalConfigurationTypes, map[string]attr.Value{ - "opsgenie_api_key": types.StringNull(), - "opsgenie_api_url": types.StringNull(), - "resolve_timeout": types.StringValue("5m"), - "smtp_auth_identity": types.StringNull(), - "smtp_auth_password": types.StringNull(), - "smtp_auth_username": types.StringNull(), - "smtp_from": types.StringValue("observability@observability.stackit.cloud"), - "smtp_smart_host": types.StringNull(), - }) - if diags.HasError() { - return alertConfigModel{}, fmt.Errorf("mapping global config: %w", core.DiagsToError(diags)) - } - - return alertConfigModel{ - Receivers: mockReceivers, - Route: mockRoute, - GlobalConfiguration: mockGlobalConfig, - }, nil -} - -func mapGlobalConfigToAttributes(respGlobalConfigs *observability.Global, globalConfigsTF *globalConfigurationModel) (basetypes.ObjectValue, error) { - if respGlobalConfigs == nil { - return types.ObjectNull(globalConfigurationTypes), nil - } - - // This bypass is needed because these values are not returned in the API GET response - smtpSmartHost := respGlobalConfigs.SmtpSmarthost - smtpAuthIdentity := respGlobalConfigs.SmtpAuthIdentity - smtpAuthPassword := respGlobalConfigs.SmtpAuthPassword - smtpAuthUsername := respGlobalConfigs.SmtpAuthUsername - opsgenieApiKey := respGlobalConfigs.OpsgenieApiKey - opsgenieApiUrl := respGlobalConfigs.OpsgenieApiUrl - if globalConfigsTF != nil { - if respGlobalConfigs.SmtpSmarthost == nil && - !globalConfigsTF.SmtpSmartHost.IsNull() && !globalConfigsTF.SmtpSmartHost.IsUnknown() { - smtpSmartHost = sdkUtils.Ptr(globalConfigsTF.SmtpSmartHost.ValueString()) - } - if respGlobalConfigs.SmtpAuthIdentity == nil && - !globalConfigsTF.SmtpAuthIdentity.IsNull() && !globalConfigsTF.SmtpAuthIdentity.IsUnknown() { - smtpAuthIdentity = sdkUtils.Ptr(globalConfigsTF.SmtpAuthIdentity.ValueString()) - } - if respGlobalConfigs.SmtpAuthPassword == nil && - !globalConfigsTF.SmtpAuthPassword.IsNull() && !globalConfigsTF.SmtpAuthPassword.IsUnknown() { - smtpAuthPassword = sdkUtils.Ptr(globalConfigsTF.SmtpAuthPassword.ValueString()) - } - if respGlobalConfigs.SmtpAuthUsername == nil && - !globalConfigsTF.SmtpAuthUsername.IsNull() && !globalConfigsTF.SmtpAuthUsername.IsUnknown() { - smtpAuthUsername = sdkUtils.Ptr(globalConfigsTF.SmtpAuthUsername.ValueString()) - } - if respGlobalConfigs.OpsgenieApiKey == nil { - opsgenieApiKey = sdkUtils.Ptr(globalConfigsTF.OpsgenieApiKey.ValueString()) - } - if respGlobalConfigs.OpsgenieApiUrl == nil { - opsgenieApiUrl = sdkUtils.Ptr(globalConfigsTF.OpsgenieApiUrl.ValueString()) - } - } - - globalConfigObject, diags := types.ObjectValue(globalConfigurationTypes, map[string]attr.Value{ - "opsgenie_api_key": types.StringPointerValue(opsgenieApiKey), - "opsgenie_api_url": types.StringPointerValue(opsgenieApiUrl), - "resolve_timeout": types.StringPointerValue(respGlobalConfigs.ResolveTimeout), - "smtp_from": types.StringPointerValue(respGlobalConfigs.SmtpFrom), - "smtp_auth_identity": types.StringPointerValue(smtpAuthIdentity), - "smtp_auth_password": types.StringPointerValue(smtpAuthPassword), - "smtp_auth_username": types.StringPointerValue(smtpAuthUsername), - "smtp_smart_host": types.StringPointerValue(smtpSmartHost), - }) - if diags.HasError() { - return types.ObjectNull(globalConfigurationTypes), fmt.Errorf("mapping global config: %w", core.DiagsToError(diags)) - } - - return globalConfigObject, nil -} - -func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observability.Receivers) (basetypes.ListValue, error) { - if respReceivers == nil { - return types.ListNull(types.ObjectType{AttrTypes: receiversTypes}), nil - } - receiversList := []attr.Value{} - emptyList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{}) - if diags.HasError() { - // Should not happen - return emptyList, fmt.Errorf("mapping empty list: %w", core.DiagsToError(diags)) - } - - if len(*respReceivers) == 0 { - return emptyList, nil - } - - for i := range *respReceivers { - receiver := (*respReceivers)[i] - - emailConfigList := []attr.Value{} - if receiver.EmailConfigs != nil { - for _, emailConfig := range *receiver.EmailConfigs { - emailConfigMap := map[string]attr.Value{ - "auth_identity": types.StringPointerValue(emailConfig.AuthIdentity), - "auth_password": types.StringPointerValue(emailConfig.AuthPassword), - "auth_username": types.StringPointerValue(emailConfig.AuthUsername), - "from": types.StringPointerValue(emailConfig.From), - "send_resolved": types.BoolPointerValue(emailConfig.SendResolved), - "smart_host": types.StringPointerValue(emailConfig.Smarthost), - "to": types.StringPointerValue(emailConfig.To), - } - emailConfigModel, diags := types.ObjectValue(emailConfigsTypes, emailConfigMap) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping email config: %w", core.DiagsToError(diags)) - } - emailConfigList = append(emailConfigList, emailConfigModel) - } - } - - opsgenieConfigList := []attr.Value{} - if receiver.OpsgenieConfigs != nil { - for _, opsgenieConfig := range *receiver.OpsgenieConfigs { - opsGenieConfigMap := map[string]attr.Value{ - "api_key": types.StringPointerValue(opsgenieConfig.ApiKey), - "api_url": types.StringPointerValue(opsgenieConfig.ApiUrl), - "tags": types.StringPointerValue(opsgenieConfig.Tags), - "priority": types.StringPointerValue(opsgenieConfig.Priority), - "send_resolved": types.BoolPointerValue(opsgenieConfig.SendResolved), - } - opsGenieConfigModel, diags := types.ObjectValue(opsgenieConfigsTypes, opsGenieConfigMap) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping opsgenie config: %w", core.DiagsToError(diags)) - } - opsgenieConfigList = append(opsgenieConfigList, opsGenieConfigModel) - } - } - - webhooksConfigList := []attr.Value{} - if receiver.WebHookConfigs != nil { - for _, webhookConfig := range *receiver.WebHookConfigs { - webHookConfigsMap := map[string]attr.Value{ - "url": types.StringPointerValue(webhookConfig.Url), - "ms_teams": types.BoolPointerValue(webhookConfig.MsTeams), - "google_chat": types.BoolPointerValue(webhookConfig.GoogleChat), - "send_resolved": types.BoolPointerValue(webhookConfig.SendResolved), - } - webHookConfigsModel, diags := types.ObjectValue(webHooksConfigsTypes, webHookConfigsMap) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping webhooks config: %w", core.DiagsToError(diags)) - } - webhooksConfigList = append(webhooksConfigList, webHookConfigsModel) - } - } - - if receiver.Name == nil { - return emptyList, fmt.Errorf("receiver name is nil") - } - - var emailConfigs basetypes.ListValue - if len(emailConfigList) == 0 { - emailConfigs = types.ListNull(types.ObjectType{AttrTypes: emailConfigsTypes}) - } else { - emailConfigs, diags = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: emailConfigsTypes}, emailConfigList) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping email configs: %w", core.DiagsToError(diags)) - } - } - - var opsGenieConfigs basetypes.ListValue - if len(opsgenieConfigList) == 0 { - opsGenieConfigs = types.ListNull(types.ObjectType{AttrTypes: opsgenieConfigsTypes}) - } else { - opsGenieConfigs, diags = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: opsgenieConfigsTypes}, opsgenieConfigList) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping opsgenie configs: %w", core.DiagsToError(diags)) - } - } - - var webHooksConfigs basetypes.ListValue - if len(webhooksConfigList) == 0 { - webHooksConfigs = types.ListNull(types.ObjectType{AttrTypes: webHooksConfigsTypes}) - } else { - webHooksConfigs, diags = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: webHooksConfigsTypes}, webhooksConfigList) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping webhooks configs: %w", core.DiagsToError(diags)) - } - } - - receiverMap := map[string]attr.Value{ - "name": types.StringPointerValue(receiver.Name), - "email_configs": emailConfigs, - "opsgenie_configs": opsGenieConfigs, - "webhooks_configs": webHooksConfigs, - } - - receiversModel, diags := types.ObjectValue(receiversTypes, receiverMap) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping receiver: %w", core.DiagsToError(diags)) - } - - receiversList = append(receiversList, receiversModel) - } - - returnReceiversList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: receiversTypes}, receiversList) - if diags.HasError() { - return emptyList, fmt.Errorf("mapping receivers list: %w", core.DiagsToError(diags)) - } - return returnReceiversList, nil -} - -func mapRouteToAttributes(ctx context.Context, route *observability.Route) (attr.Value, error) { - if route == nil { - return types.ObjectNull(mainRouteTypes), nil - } - - groupByModel, diags := types.ListValueFrom(ctx, types.StringType, route.GroupBy) - if diags.HasError() { - return types.ObjectNull(mainRouteTypes), fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) - } - - childRoutes, err := mapChildRoutesToAttributes(ctx, route.Routes) - if err != nil { - return types.ObjectNull(mainRouteTypes), fmt.Errorf("mapping child routes: %w", err) - } - - routeMap := map[string]attr.Value{ - "group_by": groupByModel, - "group_interval": types.StringPointerValue(route.GroupInterval), - "group_wait": types.StringPointerValue(route.GroupWait), - "receiver": types.StringPointerValue(route.Receiver), - "repeat_interval": types.StringPointerValue(route.RepeatInterval), - "routes": childRoutes, - } - - routeModel, diags := types.ObjectValue(mainRouteTypes, routeMap) - if diags.HasError() { - return types.ObjectNull(mainRouteTypes), fmt.Errorf("converting route to TF types: %w", core.DiagsToError(diags)) - } - - return routeModel, nil -} - -// mapChildRoutesToAttributes maps the child routes to the Terraform attributes -// This should be a recursive function to handle nested child routes -// However, the API does not currently have the correct type for the child routes -// In the future, the current implementation should be the final case of the recursive function -func mapChildRoutesToAttributes(ctx context.Context, routes *[]observability.RouteSerializer) (basetypes.ListValue, error) { - nullList := types.ListNull(getRouteListType()) - if routes == nil { - return nullList, nil - } - - routesList := []attr.Value{} - for _, route := range *routes { - groupByModel, diags := types.ListValueFrom(ctx, types.StringType, route.GroupBy) - if diags.HasError() { - return nullList, fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) - } - - matchModel, diags := types.MapValueFrom(ctx, types.StringType, route.Match) - if diags.HasError() { - return nullList, fmt.Errorf("mapping match: %w", core.DiagsToError(diags)) - } - - matchRegexModel, diags := types.MapValueFrom(ctx, types.StringType, route.MatchRe) - if diags.HasError() { - return nullList, fmt.Errorf("mapping match regex: %w", core.DiagsToError(diags)) - } - - matchersModel, diags := types.ListValueFrom(ctx, types.StringType, route.Matchers) - if diags.HasError() { - return nullList, fmt.Errorf("mapping matchers: %w", core.DiagsToError(diags)) - } - - routeMap := map[string]attr.Value{ - "continue": types.BoolPointerValue(route.Continue), - "group_by": groupByModel, - "group_interval": types.StringPointerValue(route.GroupInterval), - "group_wait": types.StringPointerValue(route.GroupWait), - "match": matchModel, - "match_regex": matchRegexModel, - "matchers": matchersModel, - "receiver": types.StringPointerValue(route.Receiver), - "repeat_interval": types.StringPointerValue(route.RepeatInterval), - } - - routeModel, diags := types.ObjectValue(getRouteListType().AttrTypes, routeMap) - if diags.HasError() { - return types.ListNull(getRouteListType()), fmt.Errorf("converting child route to TF types: %w", core.DiagsToError(diags)) - } - - routesList = append(routesList, routeModel) - } - - returnRoutesList, diags := types.ListValueFrom(ctx, getRouteListType(), routesList) - if diags.HasError() { - return nullList, fmt.Errorf("mapping child routes list: %w", core.DiagsToError(diags)) - } - return returnRoutesList, nil -} - -func toCreatePayload(model *Model) (*observability.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - elements := model.Parameters.Elements() - pa := make(map[string]interface{}, len(elements)) - for k := range elements { - pa[k] = elements[k].String() - } - return &observability.CreateInstancePayload{ - Name: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - Parameter: &pa, - }, nil -} - -func toUpdateMetricsStorageRetentionPayload(retentionDaysRaw, retentionDays5m, retentionDays1h *int64, resp *observability.GetMetricsStorageRetentionResponse) (*observability.UpdateMetricsStorageRetentionPayload, error) { - var retentionTimeRaw string - var retentionTime5m string - var retentionTime1h string - - if resp == nil || resp.MetricsRetentionTimeRaw == nil || resp.MetricsRetentionTime5m == nil || resp.MetricsRetentionTime1h == nil { - return nil, fmt.Errorf("nil response") - } - - if retentionDaysRaw == nil { - retentionTimeRaw = *resp.MetricsRetentionTimeRaw - } else { - retentionTimeRaw = fmt.Sprintf("%dd", *retentionDaysRaw) - } - - if retentionDays5m == nil { - retentionTime5m = *resp.MetricsRetentionTime5m - } else { - retentionTime5m = fmt.Sprintf("%dd", *retentionDays5m) - } - - if retentionDays1h == nil { - retentionTime1h = *resp.MetricsRetentionTime1h - } else { - retentionTime1h = fmt.Sprintf("%dd", *retentionDays1h) - } - - return &observability.UpdateMetricsStorageRetentionPayload{ - MetricsRetentionTimeRaw: &retentionTimeRaw, - MetricsRetentionTime5m: &retentionTime5m, - MetricsRetentionTime1h: &retentionTime1h, - }, nil -} - -func updateACL(ctx context.Context, projectId, instanceId string, acl []string, client *observability.APIClient) error { - payload := observability.UpdateACLPayload{ - Acl: sdkUtils.Ptr(acl), - } - - _, err := client.UpdateACL(ctx, instanceId, projectId).UpdateACLPayload(payload).Execute() - if err != nil { - return fmt.Errorf("updating ACL: %w", err) - } - - return nil -} - -func toUpdatePayload(model *Model) (*observability.UpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - elements := model.Parameters.Elements() - pa := make(map[string]interface{}, len(elements)) - for k, v := range elements { - pa[k] = v.String() - } - return &observability.UpdateInstancePayload{ - Name: conversion.StringValueToPointer(model.Name), - PlanId: conversion.StringValueToPointer(model.PlanId), - Parameter: &pa, - }, nil -} - -func toUpdateAlertConfigPayload(ctx context.Context, model *alertConfigModel) (*observability.UpdateAlertConfigsPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if model.Receivers.IsNull() || model.Receivers.IsUnknown() { - return nil, fmt.Errorf("receivers in the model are null or unknown") - } - - if model.Route.IsNull() || model.Route.IsUnknown() { - return nil, fmt.Errorf("route in the model is null or unknown") - } - - var err error - - payload := observability.UpdateAlertConfigsPayload{} - - payload.Receivers, err = toReceiverPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("mapping receivers: %w", err) - } - - routeTF := mainRouteModel{} - diags := model.Route.As(ctx, &routeTF, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("mapping route: %w", core.DiagsToError(diags)) - } - - payload.Route, err = toRoutePayload(ctx, &routeTF) - if err != nil { - return nil, fmt.Errorf("mapping route: %w", err) - } - - if !model.GlobalConfiguration.IsNull() && !model.GlobalConfiguration.IsUnknown() { - payload.Global, err = toGlobalConfigPayload(ctx, model) - if err != nil { - return nil, fmt.Errorf("mapping global: %w", err) - } - } - - return &payload, nil -} - -func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observability.UpdateAlertConfigsPayloadReceiversInner, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - receiversModel := []receiversModel{} - diags := model.Receivers.ElementsAs(ctx, &receiversModel, false) - if diags.HasError() { - return nil, fmt.Errorf("mapping receivers: %w", core.DiagsToError(diags)) - } - - receivers := []observability.UpdateAlertConfigsPayloadReceiversInner{} - - for i := range receiversModel { - receiver := receiversModel[i] - receiverPayload := observability.UpdateAlertConfigsPayloadReceiversInner{ - Name: conversion.StringValueToPointer(receiver.Name), - } - - if !receiver.EmailConfigs.IsNull() && !receiver.EmailConfigs.IsUnknown() { - emailConfigs := []emailConfigsModel{} - diags := receiver.EmailConfigs.ElementsAs(ctx, &emailConfigs, false) - if diags.HasError() { - return nil, fmt.Errorf("mapping email configs: %w", core.DiagsToError(diags)) - } - payloadEmailConfigs := []observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{} - for i := range emailConfigs { - emailConfig := emailConfigs[i] - payloadEmailConfigs = append(payloadEmailConfigs, observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{ - AuthIdentity: conversion.StringValueToPointer(emailConfig.AuthIdentity), - AuthPassword: conversion.StringValueToPointer(emailConfig.AuthPassword), - AuthUsername: conversion.StringValueToPointer(emailConfig.AuthUsername), - From: conversion.StringValueToPointer(emailConfig.From), - SendResolved: conversion.BoolValueToPointer(emailConfig.SendResolved), - Smarthost: conversion.StringValueToPointer(emailConfig.Smarthost), - To: conversion.StringValueToPointer(emailConfig.To), - }) - } - receiverPayload.EmailConfigs = &payloadEmailConfigs - } - - if !receiver.OpsGenieConfigs.IsNull() && !receiver.OpsGenieConfigs.IsUnknown() { - opsgenieConfigs := []opsgenieConfigsModel{} - diags := receiver.OpsGenieConfigs.ElementsAs(ctx, &opsgenieConfigs, false) - if diags.HasError() { - return nil, fmt.Errorf("mapping opsgenie configs: %w", core.DiagsToError(diags)) - } - payloadOpsGenieConfigs := []observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{} - for i := range opsgenieConfigs { - opsgenieConfig := opsgenieConfigs[i] - payloadOpsGenieConfigs = append(payloadOpsGenieConfigs, observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{ - ApiKey: conversion.StringValueToPointer(opsgenieConfig.ApiKey), - ApiUrl: conversion.StringValueToPointer(opsgenieConfig.ApiUrl), - Tags: conversion.StringValueToPointer(opsgenieConfig.Tags), - Priority: conversion.StringValueToPointer(opsgenieConfig.Priority), - SendResolved: conversion.BoolValueToPointer(opsgenieConfig.SendResolved), - }) - } - receiverPayload.OpsgenieConfigs = &payloadOpsGenieConfigs - } - - if !receiver.WebHooksConfigs.IsNull() && !receiver.WebHooksConfigs.IsUnknown() { - receiverWebHooksConfigs := []webHooksConfigsModel{} - diags := receiver.WebHooksConfigs.ElementsAs(ctx, &receiverWebHooksConfigs, false) - if diags.HasError() { - return nil, fmt.Errorf("mapping webhooks configs: %w", core.DiagsToError(diags)) - } - payloadWebHooksConfigs := []observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{} - for i := range receiverWebHooksConfigs { - webHooksConfig := receiverWebHooksConfigs[i] - payloadWebHooksConfigs = append(payloadWebHooksConfigs, observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{ - Url: conversion.StringValueToPointer(webHooksConfig.Url), - MsTeams: conversion.BoolValueToPointer(webHooksConfig.MsTeams), - GoogleChat: conversion.BoolValueToPointer(webHooksConfig.GoogleChat), - SendResolved: conversion.BoolValueToPointer(webHooksConfig.SendResolved), - }) - } - receiverPayload.WebHookConfigs = &payloadWebHooksConfigs - } - - receivers = append(receivers, receiverPayload) - } - return &receivers, nil -} - -func toRoutePayload(ctx context.Context, routeTF *mainRouteModel) (*observability.UpdateAlertConfigsPayloadRoute, error) { - if routeTF == nil { - return nil, fmt.Errorf("nil route model") - } - - var groupByPayload *[]string - var childRoutesPayload *[]observability.UpdateAlertConfigsPayloadRouteRoutesInner - - if !routeTF.GroupBy.IsNull() && !routeTF.GroupBy.IsUnknown() { - groupByPayload = &[]string{} - diags := routeTF.GroupBy.ElementsAs(ctx, groupByPayload, false) - if diags.HasError() { - return nil, fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) - } - } - - if !routeTF.Routes.IsNull() && !routeTF.Routes.IsUnknown() { - childRoutes := []routeModelMiddle{} - diags := routeTF.Routes.ElementsAs(ctx, &childRoutes, false) - if diags.HasError() { - // If there is an error, we will try to map the child routes as if they are the last child routes - // This is done because the last child routes in the recursion have a different structure (don't have the `routes` fields) - // and need to be unpacked to a different struct (routeModelNoRoutes) - lastChildRoutes := []routeModelNoRoutes{} - diags = routeTF.Routes.ElementsAs(ctx, &lastChildRoutes, true) - if diags.HasError() { - return nil, fmt.Errorf("mapping child routes: %w", core.DiagsToError(diags)) - } - for i := range lastChildRoutes { - childRoute := routeModelMiddle{ - Continue: lastChildRoutes[i].Continue, - GroupBy: lastChildRoutes[i].GroupBy, - GroupInterval: lastChildRoutes[i].GroupInterval, - GroupWait: lastChildRoutes[i].GroupWait, - Match: lastChildRoutes[i].Match, - MatchRegex: lastChildRoutes[i].MatchRegex, - Matchers: lastChildRoutes[i].Matchers, - Receiver: lastChildRoutes[i].Receiver, - RepeatInterval: lastChildRoutes[i].RepeatInterval, - Routes: types.ListNull(getRouteListType()), - } - childRoutes = append(childRoutes, childRoute) - } - } - - childRoutesList := []observability.UpdateAlertConfigsPayloadRouteRoutesInner{} - for i := range childRoutes { - childRoute := childRoutes[i] - childRoutePayload, err := toChildRoutePayload(ctx, &childRoute) - if err != nil { - return nil, fmt.Errorf("mapping child route: %w", err) - } - childRoutesList = append(childRoutesList, *childRoutePayload) - } - - childRoutesPayload = &childRoutesList - } - - return &observability.UpdateAlertConfigsPayloadRoute{ - GroupBy: groupByPayload, - GroupInterval: conversion.StringValueToPointer(routeTF.GroupInterval), - GroupWait: conversion.StringValueToPointer(routeTF.GroupWait), - Receiver: conversion.StringValueToPointer(routeTF.Receiver), - RepeatInterval: conversion.StringValueToPointer(routeTF.RepeatInterval), - Routes: childRoutesPayload, - }, nil -} - -func toChildRoutePayload(ctx context.Context, routeTF *routeModelMiddle) (*observability.UpdateAlertConfigsPayloadRouteRoutesInner, error) { - if routeTF == nil { - return nil, fmt.Errorf("nil route model") - } - - var groupByPayload, matchersPayload *[]string - var matchPayload, matchRegexPayload *map[string]interface{} - - if !utils.IsUndefined(routeTF.GroupBy) { - groupByPayload = &[]string{} - diags := routeTF.GroupBy.ElementsAs(ctx, groupByPayload, false) - if diags.HasError() { - return nil, fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) - } - } - - if !utils.IsUndefined(routeTF.Match) { - matchMap, err := conversion.ToStringInterfaceMap(ctx, routeTF.Match) - if err != nil { - return nil, fmt.Errorf("mapping match: %w", err) - } - matchPayload = &matchMap - } - - if !utils.IsUndefined(routeTF.MatchRegex) { - matchRegexMap, err := conversion.ToStringInterfaceMap(ctx, routeTF.MatchRegex) - if err != nil { - return nil, fmt.Errorf("mapping match regex: %w", err) - } - matchRegexPayload = &matchRegexMap - } - - if !utils.IsUndefined(routeTF.Matchers) { - matchersList, err := conversion.StringListToPointer(routeTF.Matchers) - if err != nil { - return nil, fmt.Errorf("mapping match regex: %w", err) - } - matchersPayload = matchersList - } - - return &observability.UpdateAlertConfigsPayloadRouteRoutesInner{ - Continue: conversion.BoolValueToPointer(routeTF.Continue), - GroupBy: groupByPayload, - GroupInterval: conversion.StringValueToPointer(routeTF.GroupInterval), - GroupWait: conversion.StringValueToPointer(routeTF.GroupWait), - Match: matchPayload, - MatchRe: matchRegexPayload, - Matchers: matchersPayload, - Receiver: conversion.StringValueToPointer(routeTF.Receiver), - RepeatInterval: conversion.StringValueToPointer(routeTF.RepeatInterval), - // Routes not currently supported - }, nil -} - -func toGlobalConfigPayload(ctx context.Context, model *alertConfigModel) (*observability.UpdateAlertConfigsPayloadGlobal, error) { - globalConfigModel := globalConfigurationModel{} - diags := model.GlobalConfiguration.As(ctx, &globalConfigModel, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("mapping global configuration: %w", core.DiagsToError(diags)) - } - - return &observability.UpdateAlertConfigsPayloadGlobal{ - OpsgenieApiKey: conversion.StringValueToPointer(globalConfigModel.OpsgenieApiKey), - OpsgenieApiUrl: conversion.StringValueToPointer(globalConfigModel.OpsgenieApiUrl), - ResolveTimeout: conversion.StringValueToPointer(globalConfigModel.ResolveTimeout), - SmtpAuthIdentity: conversion.StringValueToPointer(globalConfigModel.SmtpAuthIdentity), - SmtpAuthPassword: conversion.StringValueToPointer(globalConfigModel.SmtpAuthPassword), - SmtpAuthUsername: conversion.StringValueToPointer(globalConfigModel.SmtpAuthUsername), - SmtpFrom: conversion.StringValueToPointer(globalConfigModel.SmtpFrom), - SmtpSmarthost: conversion.StringValueToPointer(globalConfigModel.SmtpSmartHost), - }, nil -} - -func loadPlanId(ctx context.Context, client observability.APIClient, model *Model) (observability.Plan, error) { - projectId := model.ProjectId.ValueString() - res, err := client.ListPlans(ctx, projectId).Execute() - if err != nil { - return observability.Plan{}, err - } - - planName := model.PlanName.ValueString() - avl := "" - plans := *res.Plans - for i := range plans { - p := plans[i] - if p.Name == nil { - continue - } - if strings.EqualFold(*p.Name, planName) && p.PlanId != nil { - model.PlanId = types.StringPointerValue(p.PlanId) - return p, nil - } - avl = fmt.Sprintf("%s\n- %s", avl, *p.Name) - } - if model.PlanId.ValueString() == "" { - return observability.Plan{}, fmt.Errorf("couldn't find plan_name '%s', available names are: %s", planName, avl) - } - return observability.Plan{}, nil -} - -func (r *instanceResource) getAlertConfigs(ctx context.Context, alertConfig *alertConfigModel, model *Model) error { - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - var err error - // Alert Config - if utils.IsUndefined(model.AlertConfig) { - *alertConfig, err = getMockAlertConfig(ctx) - if err != nil { - return fmt.Errorf("Getting mock alert config: %w", err) - } - } - - alertConfigPayload, err := toUpdateAlertConfigPayload(ctx, alertConfig) - if err != nil { - return fmt.Errorf("Building alert config payload: %w", err) - } - - if alertConfigPayload != nil { - _, err = r.client.UpdateAlertConfigs(ctx, instanceId, projectId).UpdateAlertConfigsPayload(*alertConfigPayload).Execute() - if err != nil { - return fmt.Errorf("Setting alert config: %w", err) - } - } - - alertConfigResp, err := r.client.GetAlertConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - return fmt.Errorf("Calling API to get alert config: %w", err) - } - // Map response body to schema - err = mapAlertConfigField(ctx, alertConfigResp, model) - if err != nil { - return fmt.Errorf("Processing API response for the alert config: %w", err) - } - return nil -} - -func (r *instanceResource) getTracesRetention(ctx context.Context, model *Model) error { - tracesRetentionDays := conversion.Int64ValueToPointer(model.TracesRetentionDays) - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - - if tracesRetentionDays != nil { - tracesResp, err := r.client.GetTracesConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - return fmt.Errorf("Getting traces retention policy: %w", err) - } - if tracesResp == nil { - return fmt.Errorf("nil response") - } - - retentionDays := fmt.Sprintf("%dh", *tracesRetentionDays*24) - _, err = r.client.UpdateTracesConfigs(ctx, instanceId, projectId).UpdateTracesConfigsPayload(observability.UpdateTracesConfigsPayload{Retention: &retentionDays}).Execute() - if err != nil { - return fmt.Errorf("Setting traces retention policy: %w", err) - } - } - - tracesResp, err := r.client.GetTracesConfigsExecute(ctx, instanceId, projectId) - if err != nil { - return fmt.Errorf("Getting traces retention policy: %w", err) - } - - err = mapTracesRetentionField(tracesResp, model) - if err != nil { - return fmt.Errorf("Processing API response for the traces retention %w", err) - } - - return nil -} - -func (r *instanceResource) getLogsRetention(ctx context.Context, model *Model) error { - logsRetentionDays := conversion.Int64ValueToPointer(model.LogsRetentionDays) - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - - if logsRetentionDays != nil { - logsResp, err := r.client.GetLogsConfigs(ctx, instanceId, projectId).Execute() - if err != nil { - return fmt.Errorf("Getting logs retention policy: %w", err) - } - if logsResp == nil { - return fmt.Errorf("nil response") - } - - retentionDays := fmt.Sprintf("%dh", *logsRetentionDays*24) - _, err = r.client.UpdateLogsConfigs(ctx, instanceId, projectId).UpdateLogsConfigsPayload(observability.UpdateLogsConfigsPayload{Retention: &retentionDays}).Execute() - if err != nil { - return fmt.Errorf("Setting logs retention policy: %w", err) - } - } - - logsResp, err := r.client.GetLogsConfigsExecute(ctx, instanceId, projectId) - if err != nil { - return fmt.Errorf("Getting logs retention policy: %w", err) - } - - err = mapLogsRetentionField(logsResp, model) - if err != nil { - return fmt.Errorf("Processing API response for the logs retention %w", err) - } - - return nil -} - -func (r *instanceResource) getMetricsRetention(ctx context.Context, model *Model) error { - metricsRetentionDays := conversion.Int64ValueToPointer(model.MetricsRetentionDays) - metricsRetentionDays5mDownsampling := conversion.Int64ValueToPointer(model.MetricsRetentionDays5mDownsampling) - metricsRetentionDays1hDownsampling := conversion.Int64ValueToPointer(model.MetricsRetentionDays1hDownsampling) - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - - // If any of the metrics retention days are set, set the metrics retention policy - if metricsRetentionDays != nil || metricsRetentionDays5mDownsampling != nil || metricsRetentionDays1hDownsampling != nil { - // Need to get the metrics retention policy because update endpoint is a PUT and we need to send all fields - metricsResp, err := r.client.GetMetricsStorageRetentionExecute(ctx, instanceId, projectId) - if err != nil { - return fmt.Errorf("Getting metrics retention policy: %w", err) - } - - metricsRetentionPayload, err := toUpdateMetricsStorageRetentionPayload(metricsRetentionDays, metricsRetentionDays5mDownsampling, metricsRetentionDays1hDownsampling, metricsResp) - if err != nil { - return fmt.Errorf("Building metrics retention policy payload: %w", err) - } - _, err = r.client.UpdateMetricsStorageRetention(ctx, instanceId, projectId).UpdateMetricsStorageRetentionPayload(*metricsRetentionPayload).Execute() - if err != nil { - return fmt.Errorf("Setting metrics retention policy: %w", err) - } - } - - // Get metrics retention policy after update - metricsResp, err := r.client.GetMetricsStorageRetentionExecute(ctx, instanceId, projectId) - if err != nil { - return fmt.Errorf("Getting metrics retention policy: %w", err) - } - - // Map response body to schema - err = mapMetricsRetentionField(metricsResp, model) - if err != nil { - return fmt.Errorf("Processing API response for the metrics retention %w", err) - } - return nil -} - -func setACL(ctx context.Context, state *tfsdk.State, model *Model) diag.Diagnostics { - return state.SetAttribute(ctx, path.Root("acl"), model.ACL) -} - -// Needed since some plans cannot call the metrics API. -// Since the fields are optional but get a default value from the API this needs to be set for the other plans manually. -func setMetricsRetentionsZero(ctx context.Context, state *tfsdk.State) (diags diag.Diagnostics) { - diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days"), 0)...) - diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_5m_downsampling"), 0)...) - diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_1h_downsampling"), 0)...) - return diags -} - -func setMetricsRetentions(ctx context.Context, state *tfsdk.State, model *Model) (diags diag.Diagnostics) { - diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days"), model.MetricsRetentionDays)...) - diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_5m_downsampling"), model.MetricsRetentionDays5mDownsampling)...) - diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_1h_downsampling"), model.MetricsRetentionDays1hDownsampling)...) - return diags -} - -func setTracesRetentionsZero(ctx context.Context, state *tfsdk.State) (diags diag.Diagnostics) { - diags = append(diags, state.SetAttribute(ctx, path.Root("traces_retention_days"), 0)...) - return diags -} - -func setTracesRetentions(ctx context.Context, state *tfsdk.State, model *Model) (diags diag.Diagnostics) { - diags = append(diags, state.SetAttribute(ctx, path.Root("traces_retention_days"), model.TracesRetentionDays)...) - return diags -} - -func setLogsRetentionsZero(ctx context.Context, state *tfsdk.State) (diags diag.Diagnostics) { - diags = append(diags, state.SetAttribute(ctx, path.Root("logs_retention_days"), 0)...) - return diags -} - -func setLogsRetentions(ctx context.Context, state *tfsdk.State, model *Model) (diags diag.Diagnostics) { - diags = append(diags, state.SetAttribute(ctx, path.Root("logs_retention_days"), model.LogsRetentionDays)...) - return diags -} - -func setAlertConfig(ctx context.Context, state *tfsdk.State, model *Model) diag.Diagnostics { - return state.SetAttribute(ctx, path.Root("alert_config"), model.AlertConfig) -} - -type webhookConfigMutuallyExclusive struct{} - -func (v webhookConfigMutuallyExclusive) Description(_ context.Context) string { - return "ms_teams and google_chat cannot both be true" -} - -func (v webhookConfigMutuallyExclusive) MarkdownDescription(ctx context.Context) string { - return v.Description(ctx) -} - -func (v webhookConfigMutuallyExclusive) ValidateObject(_ context.Context, req validator.ObjectRequest, resp *validator.ObjectResponse) { //nolint:gocritic // req parameter signature required by validator.Object interface - if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { - return - } - - attributes := req.ConfigValue.Attributes() - - msTeamsAttr, msTeamsExists := attributes["ms_teams"] - googleChatAttr, googleChatExists := attributes["google_chat"] - - if !msTeamsExists || !googleChatExists { - return - } - - if msTeamsAttr.IsNull() || msTeamsAttr.IsUnknown() || googleChatAttr.IsNull() || googleChatAttr.IsUnknown() { - return - } - - msTeamsValue, ok1 := msTeamsAttr.(types.Bool) - googleChatValue, ok2 := googleChatAttr.(types.Bool) - - if !ok1 || !ok2 { - return - } - - if msTeamsValue.ValueBool() && googleChatValue.ValueBool() { - resp.Diagnostics.AddAttributeError( - req.Path, - "Invalid Webhook Configuration", - "Both ms_teams and google_chat cannot be set to true at the same time. Only one can be true.", - ) - } -} - -func WebhookConfigMutuallyExclusive() validator.Object { - return webhookConfigMutuallyExclusive{} -} diff --git a/stackit/internal/services/observability/instance/resource_test.go b/stackit/internal/services/observability/instance/resource_test.go deleted file mode 100644 index de145088..00000000 --- a/stackit/internal/services/observability/instance/resource_test.go +++ /dev/null @@ -1,1636 +0,0 @@ -package observability - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" -) - -func fixtureEmailConfigsModel() basetypes.ListValue { - return types.ListValueMust(types.ObjectType{AttrTypes: emailConfigsTypes}, []attr.Value{ - types.ObjectValueMust(emailConfigsTypes, map[string]attr.Value{ - "auth_identity": types.StringValue("identity"), - "auth_password": types.StringValue("password"), - "auth_username": types.StringValue("username"), - "from": types.StringValue("notification@example.com"), - "send_resolved": types.BoolValue(true), - "smart_host": types.StringValue("smtp.example.com"), - "to": types.StringValue("me@example.com"), - }), - }) -} - -func fixtureOpsGenieConfigsModel() basetypes.ListValue { - return types.ListValueMust(types.ObjectType{AttrTypes: opsgenieConfigsTypes}, []attr.Value{ - types.ObjectValueMust(opsgenieConfigsTypes, map[string]attr.Value{ - "api_key": types.StringValue("key"), - "tags": types.StringValue("tag"), - "api_url": types.StringValue("ops.example.com"), - "priority": types.StringValue("P3"), - "send_resolved": types.BoolValue(true), - }), - }) -} - -func fixtureWebHooksConfigsModel() basetypes.ListValue { - return types.ListValueMust(types.ObjectType{AttrTypes: webHooksConfigsTypes}, []attr.Value{ - types.ObjectValueMust(webHooksConfigsTypes, map[string]attr.Value{ - "url": types.StringValue("http://example.com"), - "ms_teams": types.BoolValue(true), - "google_chat": types.BoolValue(true), - "send_resolved": types.BoolValue(true), - }), - }) -} - -func fixtureReceiverModel(emailConfigs, opsGenieConfigs, webHooksConfigs basetypes.ListValue) basetypes.ObjectValue { - return types.ObjectValueMust(receiversTypes, map[string]attr.Value{ - "name": types.StringValue("name"), - "email_configs": emailConfigs, - "opsgenie_configs": opsGenieConfigs, - "webhooks_configs": webHooksConfigs, - }) -} - -func fixtureRouteModel() basetypes.ObjectValue { - return types.ObjectValueMust(mainRouteTypes, map[string]attr.Value{ - "group_by": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("label1"), - types.StringValue("label2"), - }), - "group_interval": types.StringValue("1m"), - "group_wait": types.StringValue("1m"), - "receiver": types.StringValue("name"), - "repeat_interval": types.StringValue("1m"), - // "routes": types.ListNull(getRouteListType()), - "routes": types.ListValueMust(getRouteListType(), []attr.Value{ - types.ObjectValueMust(getRouteListType().AttrTypes, map[string]attr.Value{ - "continue": types.BoolValue(false), - "group_by": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("label1"), - types.StringValue("label2"), - }), - "group_interval": types.StringValue("1m"), - "group_wait": types.StringValue("1m"), - "match": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), - "match_regex": types.MapValueMust(types.StringType, map[string]attr.Value{"key": types.StringValue("value")}), - "matchers": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("matcher1"), - types.StringValue("matcher2"), - }), - "receiver": types.StringValue("name"), - "repeat_interval": types.StringValue("1m"), - }), - }), - }) -} - -func fixtureNullRouteModel() basetypes.ObjectValue { - return types.ObjectValueMust(mainRouteTypes, map[string]attr.Value{ - "group_by": types.ListNull(types.StringType), - "group_interval": types.StringNull(), - "group_wait": types.StringNull(), - "receiver": types.StringNull(), - "repeat_interval": types.StringNull(), - "routes": types.ListNull(getRouteListType()), - }) -} - -func fixtureGlobalConfigModel() basetypes.ObjectValue { - return types.ObjectValueMust(globalConfigurationTypes, map[string]attr.Value{ - "opsgenie_api_key": types.StringValue("key"), - "opsgenie_api_url": types.StringValue("ops.example.com"), - "resolve_timeout": types.StringValue("1m"), - "smtp_auth_identity": types.StringValue("identity"), - "smtp_auth_username": types.StringValue("username"), - "smtp_auth_password": types.StringValue("password"), - "smtp_from": types.StringValue("me@example.com"), - "smtp_smart_host": types.StringValue("smtp.example.com:25"), - }) -} - -func fixtureNullGlobalConfigModel() basetypes.ObjectValue { - return types.ObjectValueMust(globalConfigurationTypes, map[string]attr.Value{ - "opsgenie_api_key": types.StringNull(), - "opsgenie_api_url": types.StringNull(), - "resolve_timeout": types.StringNull(), - "smtp_auth_identity": types.StringNull(), - "smtp_auth_username": types.StringNull(), - "smtp_auth_password": types.StringNull(), - "smtp_from": types.StringNull(), - "smtp_smart_host": types.StringNull(), - }) -} - -func fixtureEmailConfigsPayload() observability.CreateAlertConfigReceiverPayloadEmailConfigsInner { - return observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{ - AuthIdentity: utils.Ptr("identity"), - AuthPassword: utils.Ptr("password"), - AuthUsername: utils.Ptr("username"), - From: utils.Ptr("notification@example.com"), - SendResolved: utils.Ptr(true), - Smarthost: utils.Ptr("smtp.example.com"), - To: utils.Ptr("me@example.com"), - } -} - -func fixtureOpsGenieConfigsPayload() observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner { - return observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{ - ApiKey: utils.Ptr("key"), - Tags: utils.Ptr("tag"), - ApiUrl: utils.Ptr("ops.example.com"), - Priority: utils.Ptr("P3"), - SendResolved: utils.Ptr(true), - } -} - -func fixtureWebHooksConfigsPayload() observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner { - return observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{ - Url: utils.Ptr("http://example.com"), - MsTeams: utils.Ptr(true), - GoogleChat: utils.Ptr(true), - SendResolved: utils.Ptr(true), - } -} - -func fixtureReceiverPayload(emailConfigs *[]observability.CreateAlertConfigReceiverPayloadEmailConfigsInner, opsGenieConfigs *[]observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner, webHooksConfigs *[]observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner) observability.UpdateAlertConfigsPayloadReceiversInner { - return observability.UpdateAlertConfigsPayloadReceiversInner{ - EmailConfigs: emailConfigs, - Name: utils.Ptr("name"), - OpsgenieConfigs: opsGenieConfigs, - WebHookConfigs: webHooksConfigs, - } -} - -func fixtureRoutePayload() *observability.UpdateAlertConfigsPayloadRoute { - return &observability.UpdateAlertConfigsPayloadRoute{ - Continue: nil, - GroupBy: utils.Ptr([]string{"label1", "label2"}), - GroupInterval: utils.Ptr("1m"), - GroupWait: utils.Ptr("1m"), - Receiver: utils.Ptr("name"), - RepeatInterval: utils.Ptr("1m"), - Routes: &[]observability.UpdateAlertConfigsPayloadRouteRoutesInner{ - { - Continue: utils.Ptr(false), - GroupBy: utils.Ptr([]string{"label1", "label2"}), - GroupInterval: utils.Ptr("1m"), - GroupWait: utils.Ptr("1m"), - Match: &map[string]interface{}{"key": "value"}, - MatchRe: &map[string]interface{}{"key": "value"}, - Matchers: &[]string{"matcher1", "matcher2"}, - Receiver: utils.Ptr("name"), - RepeatInterval: utils.Ptr("1m"), - }, - }, - } -} - -func fixtureGlobalConfigPayload() *observability.UpdateAlertConfigsPayloadGlobal { - return &observability.UpdateAlertConfigsPayloadGlobal{ - OpsgenieApiKey: utils.Ptr("key"), - OpsgenieApiUrl: utils.Ptr("ops.example.com"), - ResolveTimeout: utils.Ptr("1m"), - SmtpAuthIdentity: utils.Ptr("identity"), - SmtpAuthUsername: utils.Ptr("username"), - SmtpAuthPassword: utils.Ptr("password"), - SmtpFrom: utils.Ptr("me@example.com"), - SmtpSmarthost: utils.Ptr("smtp.example.com:25"), - } -} - -func fixtureReceiverResponse(emailConfigs *[]observability.EmailConfig, opsGenieConfigs *[]observability.OpsgenieConfig, webhookConfigs *[]observability.WebHook) observability.Receivers { - return observability.Receivers{ - Name: utils.Ptr("name"), - EmailConfigs: emailConfigs, - OpsgenieConfigs: opsGenieConfigs, - WebHookConfigs: webhookConfigs, - } -} - -func fixtureEmailConfigsResponse() observability.EmailConfig { - return observability.EmailConfig{ - AuthIdentity: utils.Ptr("identity"), - AuthPassword: utils.Ptr("password"), - AuthUsername: utils.Ptr("username"), - From: utils.Ptr("notification@example.com"), - SendResolved: utils.Ptr(true), - Smarthost: utils.Ptr("smtp.example.com"), - To: utils.Ptr("me@example.com"), - } -} - -func fixtureOpsGenieConfigsResponse() observability.OpsgenieConfig { - return observability.OpsgenieConfig{ - ApiKey: utils.Ptr("key"), - Tags: utils.Ptr("tag"), - ApiUrl: utils.Ptr("ops.example.com"), - Priority: utils.Ptr("P3"), - SendResolved: utils.Ptr(true), - } -} - -func fixtureWebHooksConfigsResponse() observability.WebHook { - return observability.WebHook{ - Url: utils.Ptr("http://example.com"), - MsTeams: utils.Ptr(true), - GoogleChat: utils.Ptr(true), - SendResolved: utils.Ptr(true), - } -} - -func fixtureRouteResponse() *observability.Route { - return &observability.Route{ - Continue: nil, - GroupBy: utils.Ptr([]string{"label1", "label2"}), - GroupInterval: utils.Ptr("1m"), - GroupWait: utils.Ptr("1m"), - Match: &map[string]string{"key": "value"}, - MatchRe: &map[string]string{"key": "value"}, - Matchers: &[]string{"matcher1", "matcher2"}, - Receiver: utils.Ptr("name"), - RepeatInterval: utils.Ptr("1m"), - Routes: &[]observability.RouteSerializer{ - { - Continue: utils.Ptr(false), - GroupBy: utils.Ptr([]string{"label1", "label2"}), - GroupInterval: utils.Ptr("1m"), - GroupWait: utils.Ptr("1m"), - Match: &map[string]string{"key": "value"}, - MatchRe: &map[string]string{"key": "value"}, - Matchers: &[]string{"matcher1", "matcher2"}, - Receiver: utils.Ptr("name"), - RepeatInterval: utils.Ptr("1m"), - }, - }, - } -} - -func fixtureGlobalConfigResponse() *observability.Global { - return &observability.Global{ - OpsgenieApiKey: utils.Ptr("key"), - OpsgenieApiUrl: utils.Ptr("ops.example.com"), - ResolveTimeout: utils.Ptr("1m"), - SmtpAuthIdentity: utils.Ptr("identity"), - SmtpAuthUsername: utils.Ptr("username"), - SmtpAuthPassword: utils.Ptr("password"), - SmtpFrom: utils.Ptr("me@example.com"), - SmtpSmarthost: utils.Ptr("smtp.example.com:25"), - } -} - -func fixtureRouteAttributeSchema(route *schema.ListNestedAttribute, isDatasource bool) map[string]schema.Attribute { - attributeMap := map[string]schema.Attribute{ - "continue": schema.BoolAttribute{ - Description: routeDescriptions["continue"], - Optional: !isDatasource, - Computed: isDatasource, - }, - "group_by": schema.ListAttribute{ - Description: routeDescriptions["group_by"], - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "group_interval": schema.StringAttribute{ - Description: routeDescriptions["group_interval"], - Optional: !isDatasource, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "group_wait": schema.StringAttribute{ - Description: routeDescriptions["group_wait"], - Optional: !isDatasource, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "match": schema.MapAttribute{ - Description: routeDescriptions["match"], - DeprecationMessage: "Use `matchers` in the `routes` instead.", - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "match_regex": schema.MapAttribute{ - Description: routeDescriptions["match_regex"], - DeprecationMessage: "Use `matchers` in the `routes` instead.", - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "matchers": schema.ListAttribute{ - Description: routeDescriptions["matchers"], - Optional: !isDatasource, - Computed: isDatasource, - ElementType: types.StringType, - }, - "receiver": schema.StringAttribute{ - Description: routeDescriptions["receiver"], - Required: !isDatasource, - Computed: isDatasource, - }, - "repeat_interval": schema.StringAttribute{ - Description: routeDescriptions["repeat_interval"], - Optional: !isDatasource, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - } - if route != nil { - attributeMap["routes"] = *route - } - return attributeMap -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - instanceResp *observability.GetInstanceResponse - listACLResp *observability.ListACLResponse - getMetricsRetentionResp *observability.GetMetricsStorageRetentionResponse - getLogsRetentionResp *observability.LogsConfigResponse - getTracesRetentionResp *observability.TracesConfigResponse - expected Model - isValid bool - }{ - { - "default_ok", - &observability.GetInstanceResponse{ - Id: utils.Ptr("iid"), - }, - &observability.ListACLResponse{}, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.LogsConfigResponse{Config: &observability.LogsConfig{Retention: utils.Ptr("168h")}}, - &observability.TracesConfigResponse{Config: &observability.TraceConfig{Retention: utils.Ptr("168h")}}, - Model{ - Id: types.StringValue("pid,iid"), - ProjectId: types.StringValue("pid"), - InstanceId: types.StringValue("iid"), - PlanId: types.StringNull(), - PlanName: types.StringNull(), - Name: types.StringNull(), - Parameters: types.MapNull(types.StringType), - ACL: types.SetNull(types.StringType), - TracesRetentionDays: types.Int64Value(7), - LogsRetentionDays: types.Int64Value(7), - MetricsRetentionDays: types.Int64Value(60), - MetricsRetentionDays1hDownsampling: types.Int64Value(30), - MetricsRetentionDays5mDownsampling: types.Int64Value(7), - }, - true, - }, - { - "values_ok", - &observability.GetInstanceResponse{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - PlanName: utils.Ptr("plan1"), - PlanId: utils.Ptr("planId"), - Parameters: &map[string]string{"key": "value"}, - Instance: &observability.InstanceSensitiveData{ - MetricsRetentionTimeRaw: utils.Ptr(int64(60)), - MetricsRetentionTime1h: utils.Ptr(int64(30)), - MetricsRetentionTime5m: utils.Ptr(int64(7)), - }, - }, - &observability.ListACLResponse{ - Acl: &[]string{ - "1.1.1.1/32", - }, - Message: utils.Ptr("message"), - }, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.LogsConfigResponse{Config: &observability.LogsConfig{Retention: utils.Ptr("168h")}}, - &observability.TracesConfigResponse{Config: &observability.TraceConfig{Retention: utils.Ptr("168h")}}, - Model{ - Id: types.StringValue("pid,iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - InstanceId: types.StringValue("iid"), - PlanId: types.StringValue("planId"), - PlanName: types.StringValue("plan1"), - Parameters: toTerraformStringMapMust(context.Background(), map[string]string{"key": "value"}), - ACL: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("1.1.1.1/32"), - }), - TracesRetentionDays: types.Int64Value(7), - LogsRetentionDays: types.Int64Value(7), - MetricsRetentionDays: types.Int64Value(60), - MetricsRetentionDays1hDownsampling: types.Int64Value(30), - MetricsRetentionDays5mDownsampling: types.Int64Value(7), - }, - true, - }, - { - "values_ok_multiple_acls", - &observability.GetInstanceResponse{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - PlanName: utils.Ptr("plan1"), - PlanId: utils.Ptr("planId"), - Parameters: &map[string]string{"key": "value"}, - }, - &observability.ListACLResponse{ - Acl: &[]string{ - "1.1.1.1/32", - "8.8.8.8/32", - }, - Message: utils.Ptr("message"), - }, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.LogsConfigResponse{Config: &observability.LogsConfig{Retention: utils.Ptr("168h")}}, - &observability.TracesConfigResponse{Config: &observability.TraceConfig{Retention: utils.Ptr("168h")}}, - Model{ - Id: types.StringValue("pid,iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - InstanceId: types.StringValue("iid"), - PlanId: types.StringValue("planId"), - PlanName: types.StringValue("plan1"), - Parameters: toTerraformStringMapMust(context.Background(), map[string]string{"key": "value"}), - ACL: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("1.1.1.1/32"), - types.StringValue("8.8.8.8/32"), - }), - TracesRetentionDays: types.Int64Value(7), - LogsRetentionDays: types.Int64Value(7), - MetricsRetentionDays: types.Int64Value(60), - MetricsRetentionDays1hDownsampling: types.Int64Value(30), - MetricsRetentionDays5mDownsampling: types.Int64Value(7), - }, - true, - }, - { - "nullable_fields_ok", - &observability.GetInstanceResponse{ - Id: utils.Ptr("iid"), - Name: nil, - }, - &observability.ListACLResponse{ - Acl: &[]string{}, - Message: nil, - }, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.LogsConfigResponse{Config: &observability.LogsConfig{Retention: utils.Ptr("168h")}}, - &observability.TracesConfigResponse{Config: &observability.TraceConfig{Retention: utils.Ptr("168h")}}, - Model{ - Id: types.StringValue("pid,iid"), - ProjectId: types.StringValue("pid"), - InstanceId: types.StringValue("iid"), - PlanId: types.StringNull(), - PlanName: types.StringNull(), - Name: types.StringNull(), - Parameters: types.MapNull(types.StringType), - ACL: types.SetNull(types.StringType), - TracesRetentionDays: types.Int64Value(7), - LogsRetentionDays: types.Int64Value(7), - MetricsRetentionDays: types.Int64Value(60), - MetricsRetentionDays1hDownsampling: types.Int64Value(30), - MetricsRetentionDays5mDownsampling: types.Int64Value(7), - }, - true, - }, - { - "response_nil_fail", - nil, - nil, - nil, - nil, - nil, - Model{}, - false, - }, - { - "no_resource_id", - &observability.GetInstanceResponse{}, - nil, - nil, - nil, - nil, - Model{}, - false, - }, - { - "empty metrics retention", - &observability.GetInstanceResponse{ - Id: utils.Ptr("iid"), - Name: nil, - }, - &observability.ListACLResponse{ - Acl: &[]string{}, - Message: nil, - }, - &observability.GetMetricsStorageRetentionResponse{}, - &observability.LogsConfigResponse{}, - &observability.TracesConfigResponse{}, - Model{}, - false, - }, - { - "nil metrics retention", - &observability.GetInstanceResponse{ - Id: utils.Ptr("iid"), - Name: nil, - }, - &observability.ListACLResponse{ - Acl: &[]string{}, - Message: nil, - }, - nil, - nil, - nil, - Model{}, - false, - }, - { - "update metrics retention", - &observability.GetInstanceResponse{ - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - PlanName: utils.Ptr("plan1"), - PlanId: utils.Ptr("planId"), - Parameters: &map[string]string{"key": "value"}, - Instance: &observability.InstanceSensitiveData{ - MetricsRetentionTimeRaw: utils.Ptr(int64(30)), - MetricsRetentionTime1h: utils.Ptr(int64(15)), - MetricsRetentionTime5m: utils.Ptr(int64(10)), - }, - }, - &observability.ListACLResponse{ - Acl: &[]string{ - "1.1.1.1/32", - }, - Message: utils.Ptr("message"), - }, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.LogsConfigResponse{Config: &observability.LogsConfig{Retention: utils.Ptr("480h")}}, - &observability.TracesConfigResponse{Config: &observability.TraceConfig{Retention: utils.Ptr("720h")}}, - Model{ - Id: types.StringValue("pid,iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - InstanceId: types.StringValue("iid"), - PlanId: types.StringValue("planId"), - PlanName: types.StringValue("plan1"), - Parameters: toTerraformStringMapMust(context.Background(), map[string]string{"key": "value"}), - ACL: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("1.1.1.1/32"), - }), - LogsRetentionDays: types.Int64Value(20), - TracesRetentionDays: types.Int64Value(30), - MetricsRetentionDays: types.Int64Value(60), - MetricsRetentionDays1hDownsampling: types.Int64Value(30), - MetricsRetentionDays5mDownsampling: types.Int64Value(7), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - ACL: types.SetNull(types.StringType), - } - err := mapFields(context.Background(), tt.instanceResp, state) - aclErr := mapACLField(tt.listACLResp, state) - metricsErr := mapMetricsRetentionField(tt.getMetricsRetentionResp, state) - logsErr := mapLogsRetentionField(tt.getLogsRetentionResp, state) - tracesErr := mapTracesRetentionField(tt.getTracesRetentionResp, state) - if !tt.isValid && err == nil && aclErr == nil && metricsErr == nil && logsErr == nil && tracesErr == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && (err != nil || aclErr != nil || metricsErr != nil || logsErr != nil || tracesErr != nil) { - t.Fatalf("Should not have failed: %v", err) - } - - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapAlertConfigField(t *testing.T) { - tests := []struct { - description string - alertConfigResp *observability.GetAlertConfigsResponse - expected Model - isValid bool - }{ - { - description: "basic_ok", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{ - fixtureReceiverResponse( - &[]observability.EmailConfig{ - fixtureEmailConfigsResponse(), - }, - &[]observability.OpsgenieConfig{ - fixtureOpsGenieConfigsResponse(), - }, - &[]observability.WebHook{ - fixtureWebHooksConfigsResponse(), - }, - ), - }, - Route: fixtureRouteResponse(), - Global: fixtureGlobalConfigResponse(), - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - }), - "route": fixtureRouteModel(), - "global": fixtureGlobalConfigModel(), - }), - }, - isValid: true, - }, - { - description: "receivers only emailconfigs", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{ - fixtureReceiverResponse( - &[]observability.EmailConfig{ - fixtureEmailConfigsResponse(), - }, - nil, - nil, - ), - }, - Route: fixtureRouteResponse(), - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - types.ListNull(types.ObjectType{AttrTypes: opsgenieConfigsTypes}), - types.ListNull(types.ObjectType{AttrTypes: webHooksConfigsTypes}), - ), - }), - "route": fixtureRouteModel(), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "receivers only opsgenieconfigs", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{ - fixtureReceiverResponse( - nil, - &[]observability.OpsgenieConfig{ - fixtureOpsGenieConfigsResponse(), - }, - nil, - ), - }, - Route: fixtureRouteResponse(), - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - types.ListNull(types.ObjectType{AttrTypes: emailConfigsTypes}), - fixtureOpsGenieConfigsModel(), - types.ListNull(types.ObjectType{AttrTypes: webHooksConfigsTypes}), - ), - }), - "route": fixtureRouteModel(), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "receivers only webhooksconfigs", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{ - fixtureReceiverResponse( - nil, - nil, - &[]observability.WebHook{ - fixtureWebHooksConfigsResponse(), - }, - ), - }, - Route: fixtureRouteResponse(), - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - types.ListNull(types.ObjectType{AttrTypes: emailConfigsTypes}), - types.ListNull(types.ObjectType{AttrTypes: opsgenieConfigsTypes}), - fixtureWebHooksConfigsModel(), - ), - }), - "route": fixtureRouteModel(), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "no receivers, no routes", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{}, - Route: &observability.Route{}, - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{}), - "route": fixtureNullRouteModel(), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "no receivers, default routes", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{}, - Route: fixtureRouteResponse(), - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{}), - "route": fixtureRouteModel(), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "default receivers, no routes", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{ - fixtureReceiverResponse( - &[]observability.EmailConfig{ - fixtureEmailConfigsResponse(), - }, - &[]observability.OpsgenieConfig{ - fixtureOpsGenieConfigsResponse(), - }, - &[]observability.WebHook{ - fixtureWebHooksConfigsResponse(), - }, - ), - }, - Route: &observability.Route{}, - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - }), - "route": fixtureNullRouteModel(), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "nil receivers", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: nil, - Route: fixtureRouteResponse(), - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListNull(types.ObjectType{AttrTypes: receiversTypes}), - "route": fixtureRouteModel(), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "nil route", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{ - fixtureReceiverResponse( - &[]observability.EmailConfig{ - fixtureEmailConfigsResponse(), - }, - &[]observability.OpsgenieConfig{ - fixtureOpsGenieConfigsResponse(), - }, - &[]observability.WebHook{ - fixtureWebHooksConfigsResponse(), - }, - ), - }, - Route: nil, - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - }), - "route": types.ObjectNull(mainRouteTypes), - "global": types.ObjectNull(globalConfigurationTypes), - }), - }, - isValid: true, - }, - { - description: "empty global options", - alertConfigResp: &observability.GetAlertConfigsResponse{ - Data: &observability.Alert{ - Receivers: &[]observability.Receivers{ - fixtureReceiverResponse( - &[]observability.EmailConfig{ - fixtureEmailConfigsResponse(), - }, - &[]observability.OpsgenieConfig{ - fixtureOpsGenieConfigsResponse(), - }, - &[]observability.WebHook{ - fixtureWebHooksConfigsResponse(), - }, - ), - }, - Route: fixtureRouteResponse(), - Global: &observability.Global{}, - }, - }, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectValueMust(alertConfigTypes, map[string]attr.Value{ - "receivers": types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - }), - "route": fixtureRouteModel(), - "global": fixtureNullGlobalConfigModel(), - }), - }, - isValid: true, - }, - { - description: "nil resp", - alertConfigResp: nil, - expected: Model{ - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - AlertConfig: types.ObjectNull(receiversTypes), - }, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - ACL: types.SetNull(types.StringType), - Parameters: types.MapNull(types.StringType), - } - err := mapAlertConfigField(context.Background(), tt.alertConfigResp, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - - if tt.isValid { - diff := cmp.Diff(state.AlertConfig, tt.expected.AlertConfig) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *observability.CreateInstancePayload - isValid bool - }{ - { - "basic_ok", - &Model{ - PlanId: types.StringValue("planId"), - }, - &observability.CreateInstancePayload{ - Name: nil, - PlanId: utils.Ptr("planId"), - Parameter: &map[string]interface{}{}, - }, - true, - }, - { - "ok", - &Model{ - Name: types.StringValue("Name"), - PlanId: types.StringValue("planId"), - Parameters: makeTestMap(t), - }, - &observability.CreateInstancePayload{ - Name: utils.Ptr("Name"), - PlanId: utils.Ptr("planId"), - Parameter: &map[string]interface{}{"key": `"value"`}, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToPayloadUpdate(t *testing.T) { - tests := []struct { - description string - input *Model - expected *observability.UpdateInstancePayload - isValid bool - }{ - { - "basic_ok", - &Model{ - PlanId: types.StringValue("planId"), - }, - &observability.UpdateInstancePayload{ - Name: nil, - PlanId: utils.Ptr("planId"), - Parameter: &map[string]any{}, - }, - true, - }, - { - "ok", - &Model{ - Name: types.StringValue("Name"), - PlanId: types.StringValue("planId"), - Parameters: makeTestMap(t), - }, - &observability.UpdateInstancePayload{ - Name: utils.Ptr("Name"), - PlanId: utils.Ptr("planId"), - Parameter: &map[string]any{"key": `"value"`}, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdateMetricsStorageRetentionPayload(t *testing.T) { - tests := []struct { - description string - retentionDaysRaw *int64 - retentionDays1h *int64 - retentionDays5m *int64 - getMetricsResp *observability.GetMetricsStorageRetentionResponse - expected *observability.UpdateMetricsStorageRetentionPayload - isValid bool - }{ - { - "basic_ok", - utils.Ptr(int64(120)), - utils.Ptr(int64(60)), - utils.Ptr(int64(14)), - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.UpdateMetricsStorageRetentionPayload{ - MetricsRetentionTimeRaw: utils.Ptr("120d"), - MetricsRetentionTime1h: utils.Ptr("60d"), - MetricsRetentionTime5m: utils.Ptr("14d"), - }, - true, - }, - { - "only_raw_given", - utils.Ptr(int64(120)), - nil, - nil, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.UpdateMetricsStorageRetentionPayload{ - MetricsRetentionTimeRaw: utils.Ptr("120d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - true, - }, - { - "only_1h_given", - nil, - utils.Ptr(int64(60)), - nil, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.UpdateMetricsStorageRetentionPayload{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("60d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - true, - }, - { - "only_5m_given", - nil, - nil, - utils.Ptr(int64(14)), - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.UpdateMetricsStorageRetentionPayload{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("14d"), - }, - true, - }, - { - "none_given", - nil, - nil, - nil, - &observability.GetMetricsStorageRetentionResponse{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - &observability.UpdateMetricsStorageRetentionPayload{ - MetricsRetentionTimeRaw: utils.Ptr("60d"), - MetricsRetentionTime1h: utils.Ptr("30d"), - MetricsRetentionTime5m: utils.Ptr("7d"), - }, - true, - }, - { - "nil_response", - nil, - nil, - nil, - nil, - nil, - false, - }, - { - "empty_response", - nil, - nil, - nil, - &observability.GetMetricsStorageRetentionResponse{}, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdateMetricsStorageRetentionPayload(tt.retentionDaysRaw, tt.retentionDays5m, tt.retentionDays1h, tt.getMetricsResp) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdateAlertConfigPayload(t *testing.T) { - tests := []struct { - description string - input alertConfigModel - expected *observability.UpdateAlertConfigsPayload - isValid bool - }{ - { - description: "base", - input: alertConfigModel{ - Receivers: types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - }), - Route: fixtureRouteModel(), - GlobalConfiguration: fixtureGlobalConfigModel(), - }, - expected: &observability.UpdateAlertConfigsPayload{ - Receivers: &[]observability.UpdateAlertConfigsPayloadReceiversInner{ - fixtureReceiverPayload( - &[]observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{fixtureEmailConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{fixtureOpsGenieConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{fixtureWebHooksConfigsPayload()}, - ), - }, - Route: fixtureRoutePayload(), - Global: fixtureGlobalConfigPayload(), - }, - isValid: true, - }, - { - description: "receivers only emailconfigs", - input: alertConfigModel{ - Receivers: types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - types.ListNull(types.ObjectType{AttrTypes: opsgenieConfigsTypes}), - types.ListNull(types.ObjectType{AttrTypes: webHooksConfigsTypes}), - ), - }), - Route: fixtureRouteModel(), - }, - expected: &observability.UpdateAlertConfigsPayload{ - Receivers: &[]observability.UpdateAlertConfigsPayloadReceiversInner{ - fixtureReceiverPayload( - &[]observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{fixtureEmailConfigsPayload()}, - nil, - nil, - ), - }, - Route: fixtureRoutePayload(), - }, - isValid: true, - }, - { - description: "receivers only opsgenieconfigs", - input: alertConfigModel{ - Receivers: types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - types.ListNull(types.ObjectType{AttrTypes: emailConfigsTypes}), - fixtureOpsGenieConfigsModel(), - types.ListNull(types.ObjectType{AttrTypes: webHooksConfigsTypes}), - ), - }), - Route: fixtureRouteModel(), - }, - expected: &observability.UpdateAlertConfigsPayload{ - Receivers: &[]observability.UpdateAlertConfigsPayloadReceiversInner{ - fixtureReceiverPayload( - nil, - &[]observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{fixtureOpsGenieConfigsPayload()}, - nil, - ), - }, - Route: fixtureRoutePayload(), - }, - isValid: true, - }, - { - description: "multiple receivers", - input: alertConfigModel{ - Receivers: types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - }), - Route: fixtureRouteModel(), - }, - expected: &observability.UpdateAlertConfigsPayload{ - Receivers: &[]observability.UpdateAlertConfigsPayloadReceiversInner{ - fixtureReceiverPayload( - &[]observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{fixtureEmailConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{fixtureOpsGenieConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{fixtureWebHooksConfigsPayload()}, - ), - fixtureReceiverPayload( - &[]observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{fixtureEmailConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{fixtureOpsGenieConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{fixtureWebHooksConfigsPayload()}, - ), - }, - Route: fixtureRoutePayload(), - }, - isValid: true, - }, - { - description: "empty global options", - input: alertConfigModel{ - Receivers: types.ListValueMust(types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{ - fixtureReceiverModel( - fixtureEmailConfigsModel(), - fixtureOpsGenieConfigsModel(), - fixtureWebHooksConfigsModel(), - ), - }), - Route: fixtureRouteModel(), - GlobalConfiguration: fixtureNullGlobalConfigModel(), - }, - expected: &observability.UpdateAlertConfigsPayload{ - Receivers: &[]observability.UpdateAlertConfigsPayloadReceiversInner{ - fixtureReceiverPayload( - &[]observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{fixtureEmailConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{fixtureOpsGenieConfigsPayload()}, - &[]observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{fixtureWebHooksConfigsPayload()}, - ), - }, - Route: fixtureRoutePayload(), - Global: &observability.UpdateAlertConfigsPayloadGlobal{}, - }, - isValid: true, - }, - { - description: "empty alert config", - input: alertConfigModel{}, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdateAlertConfigPayload(context.Background(), &tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestGetRouteNestedObjectAux(t *testing.T) { - tests := []struct { - description string - startingLevel int - recursionLimit int - isDatasource bool - expected schema.ListNestedAttribute - }{ - { - "no recursion, resource", - 1, - 1, - false, - schema.ListNestedAttribute{ - Description: routeDescriptions["routes"], - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: fixtureRouteAttributeSchema(nil, false), - }, - }, - }, - { - "recursion 1, resource", - 1, - 2, - false, - schema.ListNestedAttribute{ - Description: routeDescriptions["routes"], - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: fixtureRouteAttributeSchema( - &schema.ListNestedAttribute{ - Description: routeDescriptions["routes"], - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: fixtureRouteAttributeSchema(nil, false), - }, - }, - false, - ), - }, - }, - }, - { - "no recursion,datasource", - 1, - 1, - true, - schema.ListNestedAttribute{ - Description: routeDescriptions["routes"], - Computed: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: fixtureRouteAttributeSchema(nil, true), - }, - }, - }, - { - "recursion 1, datasource", - 1, - 2, - true, - schema.ListNestedAttribute{ - Description: routeDescriptions["routes"], - Computed: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: fixtureRouteAttributeSchema( - &schema.ListNestedAttribute{ - Description: routeDescriptions["routes"], - Computed: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: fixtureRouteAttributeSchema(nil, true), - }, - }, - true, - ), - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output := getRouteNestedObjectAux(tt.isDatasource, tt.startingLevel, tt.recursionLimit) - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func TestGetRouteListTypeAux(t *testing.T) { - tests := []struct { - description string - startingLevel int - recursionLimit int - expected types.ObjectType - }{ - { - "no recursion", - 1, - 1, - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "continue": types.BoolType, - "group_by": types.ListType{ElemType: types.StringType}, - "group_interval": types.StringType, - "group_wait": types.StringType, - "match": types.MapType{ElemType: types.StringType}, - "match_regex": types.MapType{ElemType: types.StringType}, - "matchers": types.ListType{ElemType: types.StringType}, - "receiver": types.StringType, - "repeat_interval": types.StringType, - }, - }, - }, - { - "recursion 1", - 1, - 2, - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "continue": types.BoolType, - "group_by": types.ListType{ElemType: types.StringType}, - "group_interval": types.StringType, - "group_wait": types.StringType, - "match": types.MapType{ElemType: types.StringType}, - "match_regex": types.MapType{ElemType: types.StringType}, - "matchers": types.ListType{ElemType: types.StringType}, - "receiver": types.StringType, - "repeat_interval": types.StringType, - "routes": types.ListType{ElemType: types.ObjectType{AttrTypes: map[string]attr.Type{ - "continue": types.BoolType, - "group_by": types.ListType{ElemType: types.StringType}, - "group_interval": types.StringType, - "group_wait": types.StringType, - "match": types.MapType{ElemType: types.StringType}, - "match_regex": types.MapType{ElemType: types.StringType}, - "matchers": types.ListType{ElemType: types.StringType}, - "receiver": types.StringType, - "repeat_interval": types.StringType, - }}}, - }, - }, - }, - { - "recursion 2", - 2, - 2, - types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "continue": types.BoolType, - "group_by": types.ListType{ElemType: types.StringType}, - "group_interval": types.StringType, - "group_wait": types.StringType, - "match": types.MapType{ElemType: types.StringType}, - "match_regex": types.MapType{ElemType: types.StringType}, - "matchers": types.ListType{ElemType: types.StringType}, - "receiver": types.StringType, - "repeat_interval": types.StringType, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output := getRouteListTypeAux(tt.startingLevel, tt.recursionLimit) - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - }) - } -} - -func makeTestMap(t *testing.T) basetypes.MapValue { - p := make(map[string]attr.Value, 1) - p["key"] = types.StringValue("value") - params, diag := types.MapValueFrom(context.Background(), types.StringType, p) - if diag.HasError() { - t.Fail() - } - return params -} - -// ToTerraformStringMapMust Silently ignores the error -func toTerraformStringMapMust(ctx context.Context, m map[string]string) basetypes.MapValue { - labels := make(map[string]attr.Value, len(m)) - for l, v := range m { - stringValue := types.StringValue(v) - labels[l] = stringValue - } - res, diags := types.MapValueFrom(ctx, types.StringType, m) - if diags.HasError() { - return types.MapNull(types.StringType) - } - return res -} diff --git a/stackit/internal/services/observability/log-alertgroup/datasource.go b/stackit/internal/services/observability/log-alertgroup/datasource.go deleted file mode 100644 index 77a55476..00000000 --- a/stackit/internal/services/observability/log-alertgroup/datasource.go +++ /dev/null @@ -1,173 +0,0 @@ -package logalertgroup - -import ( - "context" - "errors" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &logAlertGroupDataSource{} -) - -// NewLogAlertGroupDataSource creates a new instance of the alertGroupDataSource. -func NewLogAlertGroupDataSource() datasource.DataSource { - return &logAlertGroupDataSource{} -} - -// alertGroupDataSource is the datasource implementation. -type logAlertGroupDataSource struct { - client *observability.APIClient -} - -// Configure adds the provider configured client to the resource. -func (l *logAlertGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - l.client = apiClient - tflog.Info(ctx, "Observability log alert group client configured") -} - -// Metadata provides metadata for the log alert group datasource. -func (l *logAlertGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_logalertgroup" -} - -// Schema defines the schema for the log alert group data source. -func (l *logAlertGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability log alert group datasource schema. Used to create alerts based on logs (Loki). Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - stringvalidator.LengthBetween(1, 200), - }, - }, - "interval": schema.StringAttribute{ - Description: descriptions["interval"], - Computed: true, - Validators: []validator.String{ - validate.ValidDurationString(), - }, - }, - "rules": schema.ListNestedAttribute{ - Description: descriptions["rules"], - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "alert": schema.StringAttribute{ - Description: descriptions["alert"], - Computed: true, - }, - "expression": schema.StringAttribute{ - Description: descriptions["expression"], - Computed: true, - }, - "for": schema.StringAttribute{ - Description: descriptions["for"], - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - ElementType: types.StringType, - Computed: true, - }, - "annotations": schema.MapAttribute{ - Description: descriptions["annotations"], - ElementType: types.StringType, - Computed: true, - }, - }, - }, - }, - }, - } -} - -func (l *logAlertGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "log_alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - readAlertGroupResp, err := l.client.GetLogsAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading log alert group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, readAlertGroupResp.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading log alert group", fmt.Sprintf("Error processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) -} diff --git a/stackit/internal/services/observability/log-alertgroup/resource.go b/stackit/internal/services/observability/log-alertgroup/resource.go deleted file mode 100644 index 855b6300..00000000 --- a/stackit/internal/services/observability/log-alertgroup/resource.go +++ /dev/null @@ -1,574 +0,0 @@ -package logalertgroup - -import ( - "context" - "errors" - "fmt" - "net/http" - "regexp" - "strings" - - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "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/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &logAlertGroupResource{} - _ resource.ResourceWithConfigure = &logAlertGroupResource{} - _ resource.ResourceWithImportState = &logAlertGroupResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - InstanceId types.String `tfsdk:"instance_id"` - Name types.String `tfsdk:"name"` - Interval types.String `tfsdk:"interval"` - Rules types.List `tfsdk:"rules"` -} - -type rule struct { - Alert types.String `tfsdk:"alert"` - Annotations types.Map `tfsdk:"annotations"` - Labels types.Map `tfsdk:"labels"` - Expression types.String `tfsdk:"expression"` - For types.String `tfsdk:"for"` -} - -var ruleTypes = map[string]attr.Type{ - "alert": basetypes.StringType{}, - "annotations": basetypes.MapType{ElemType: types.StringType}, - "labels": basetypes.MapType{ElemType: types.StringType}, - "expression": basetypes.StringType{}, - "for": basetypes.StringType{}, -} - -// Descriptions for the resource and data source schemas are centralized here. -var descriptions = map[string]string{ - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`name`\".", - "project_id": "STACKIT project ID to which the log alert group is associated.", - "instance_id": "Observability instance ID to which the log alert group is associated.", - "name": "The name of the log alert group. Is the identifier and must be unique in the group.", - "interval": "Specifies the frequency at which rules within the group are evaluated. The interval must be at least 60 seconds and defaults to 60 seconds if not set. Supported formats include hours, minutes, and seconds, either singly or in combination. Examples of valid formats are: '5h30m40s', '5h', '5h30m', '60m', and '60s'.", - "alert": "The name of the alert rule. Is the identifier and must be unique in the group.", - "expression": "The LogQL expression to evaluate. Every evaluation cycle this is evaluated at the current time, and all resultant time series become pending/firing alerts.", - "for": "Alerts are considered firing once they have been returned for this long. Alerts which have not yet fired for long enough are considered pending. Default is 0s", - "labels": "A map of key:value. Labels to add or overwrite for each alert", - "annotations": "A map of key:value. Annotations to add or overwrite for each alert", -} - -// NewLogAlertGroupResource is a helper function to simplify the provider implementation. -func NewLogAlertGroupResource() resource.Resource { - return &logAlertGroupResource{} -} - -// alertGroupResource is the resource implementation. -type logAlertGroupResource struct { - client *observability.APIClient -} - -// Metadata returns the resource type name. -func (l *logAlertGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_logalertgroup" -} - -// Configure adds the provider configured client to the resource. -func (l *logAlertGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - l.client = apiClient - tflog.Info(ctx, "Observability log alert group client configured") -} - -// Schema defines the schema for the resource. -func (l *logAlertGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability log alert group resource schema. Used to create alerts based on logs (Loki). Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - stringvalidator.LengthBetween(1, 200), - stringvalidator.RegexMatches( - regexp.MustCompile(`^[a-zA-Z0-9-]+$`), - "must match expression", - ), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "interval": schema.StringAttribute{ - Description: descriptions["interval"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.ValidDurationString(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "rules": schema.ListNestedAttribute{ - Description: "Rules for the log alert group", - Required: true, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "alert": schema.StringAttribute{ - Description: descriptions["alert"], - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile(`^[a-zA-Z0-9-]+$`), - "must match expression", - ), - stringvalidator.LengthBetween(1, 200), - }, - }, - "expression": schema.StringAttribute{ - Description: descriptions["expression"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 600), - // The API currently accepts expressions with trailing newlines but does not return them, - // leading to inconsistent Terraform results. This issue has been reported to the Obs team. - // Until it is resolved, we proactively notify users if their input contains a trailing newline. - validate.ValidNoTrailingNewline(), - }, - }, - "for": schema.StringAttribute{ - Description: descriptions["for"], - Optional: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(2, 8), - validate.ValidDurationString(), - }, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - Optional: true, - ElementType: types.StringType, - Validators: []validator.Map{ - mapvalidator.KeysAre(stringvalidator.LengthAtMost(200)), - mapvalidator.ValueStringsAre(stringvalidator.LengthAtMost(200)), - mapvalidator.SizeAtMost(10), - }, - }, - "annotations": schema.MapAttribute{ - Description: descriptions["annotations"], - Optional: true, - ElementType: types.StringType, - Validators: []validator.Map{ - mapvalidator.KeysAre(stringvalidator.LengthAtMost(200)), - mapvalidator.ValueStringsAre(stringvalidator.LengthAtMost(200)), - mapvalidator.SizeAtMost(5), - }, - }, - }, - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (l *logAlertGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "log_alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - payload, err := toCreatePayload(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating alertgroup", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - createAlertGroupResp, err := l.client.CreateLogsAlertgroups(ctx, instanceId, projectId).CreateLogsAlertgroupsPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating alertgroup", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // all log alert groups are returned. We have to search the map for the one corresponding to our name - for _, alertGroup := range *createAlertGroupResp.Data { - if model.Name.ValueString() != *alertGroup.Name { - continue - } - - err = mapFields(ctx, &alertGroup, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating log alert group", fmt.Sprintf("Processing API payload: %v", err)) - return - } - } - - // Set the state with fully populated data. - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "log alert group created") -} - -// Read refreshes the Terraform state with the latest data. -func (l *logAlertGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "log_alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - readAlertGroupResp, err := l.client.GetLogsAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading log alert group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, readAlertGroupResp.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading log alert group", fmt.Sprintf("Error processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) -} - -// Update attempts to update the resource. In this case, alertgroups cannot be updated. -// The Update function is redundant since any modifications will -// automatically trigger a resource recreation through Terraform's built-in -// lifecycle management. -func (l *logAlertGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating log alert group", "Observability log alert groups can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (l *logAlertGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - alertGroupName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "log_alert_group_name", alertGroupName) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - _, err := l.client.DeleteLogsAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting log alert group", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "log alert group deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,name -func (l *logAlertGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing scrape config", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id],[name] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "Observability log alert group state imported") -} - -// toCreatePayload generates the payload to create a new log alert group. -func toCreatePayload(ctx context.Context, model *Model) (*observability.CreateLogsAlertgroupsPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payload := observability.CreateLogsAlertgroupsPayload{} - - if !utils.IsUndefined(model.Name) { - payload.Name = model.Name.ValueStringPointer() - } - - if !utils.IsUndefined(model.Interval) { - payload.Interval = model.Interval.ValueStringPointer() - } - - if !utils.IsUndefined(model.Rules) { - rules, err := toRulesPayload(ctx, model) - if err != nil { - return nil, err - } - payload.Rules = &rules - } - - return &payload, nil -} - -// toRulesPayload generates rules for create payload. -func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAlertgroupsRequestInnerRulesInner, error) { - if model.Rules.Elements() == nil || len(model.Rules.Elements()) == 0 { - return []observability.UpdateAlertgroupsRequestInnerRulesInner{}, nil - } - - var rules []rule - diags := model.Rules.ElementsAs(ctx, &rules, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - var oarrs []observability.UpdateAlertgroupsRequestInnerRulesInner - for i := range rules { - rule := &rules[i] - oarr := observability.UpdateAlertgroupsRequestInnerRulesInner{} - - if !utils.IsUndefined(rule.Alert) { - alert := conversion.StringValueToPointer(rule.Alert) - if alert == nil { - return nil, fmt.Errorf("found nil alert for rule[%d]", i) - } - oarr.Alert = alert - } - - if !utils.IsUndefined(rule.Expression) { - expression := conversion.StringValueToPointer(rule.Expression) - if expression == nil { - return nil, fmt.Errorf("found nil expression for rule[%d]", i) - } - oarr.Expr = expression - } - - if !utils.IsUndefined(rule.For) { - for_ := conversion.StringValueToPointer(rule.For) - if for_ == nil { - return nil, fmt.Errorf("found nil expression for for_[%d]", i) - } - oarr.For = for_ - } - - if !utils.IsUndefined(rule.Labels) { - labels, err := conversion.ToStringInterfaceMap(ctx, rule.Labels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - oarr.Labels = &labels - } - - if !utils.IsUndefined(rule.Annotations) { - annotations, err := conversion.ToStringInterfaceMap(ctx, rule.Annotations) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - oarr.Annotations = &annotations - } - - oarrs = append(oarrs, oarr) - } - - return oarrs, nil -} - -// mapRules maps alertGroup response to the model. -func mapFields(ctx context.Context, alertGroup *observability.AlertGroup, model *Model) error { - if alertGroup == nil { - return fmt.Errorf("nil alertGroup") - } - - if model == nil { - return fmt.Errorf("nil model") - } - - if utils.IsUndefined(model.Name) { - return fmt.Errorf("empty name") - } - - if utils.IsUndefined(model.ProjectId) { - return fmt.Errorf("empty projectId") - } - - if utils.IsUndefined(model.InstanceId) { - return fmt.Errorf("empty instanceId") - } - - var name string - if !utils.IsUndefined(model.Name) { - name = model.Name.ValueString() - } else if alertGroup.Name != nil { - name = *alertGroup.Name - } else { - return fmt.Errorf("found empty name") - } - - model.Name = types.StringValue(name) - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), name) - - var interval string - if !utils.IsUndefined(model.Interval) { - interval = model.Interval.ValueString() - } else if alertGroup.Interval != nil { - interval = *alertGroup.Interval - } else { - return fmt.Errorf("found empty interval") - } - model.Interval = types.StringValue(interval) - - if alertGroup.Rules != nil { - err := mapRules(ctx, alertGroup, model) - if err != nil { - return fmt.Errorf("map rules: %w", err) - } - } - - return nil -} - -// mapRules maps alertGroup response rules to the model rules. -func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Model) error { - var newRules []attr.Value - - for i, r := range *alertGroup.Rules { - ruleMap := map[string]attr.Value{ - "alert": types.StringPointerValue(r.Alert), - "expression": types.StringPointerValue(r.Expr), - "for": types.StringPointerValue(r.For), - "labels": types.MapNull(types.StringType), - "annotations": types.MapNull(types.StringType), - } - - if r.Labels != nil { - labelElems := map[string]attr.Value{} - for k, v := range *r.Labels { - labelElems[k] = types.StringValue(v) - } - ruleMap["labels"] = types.MapValueMust(types.StringType, labelElems) - } - - if r.Annotations != nil { - annoElems := map[string]attr.Value{} - for k, v := range *r.Annotations { - annoElems[k] = types.StringValue(v) - } - ruleMap["annotations"] = types.MapValueMust(types.StringType, annoElems) - } - - ruleTf, diags := types.ObjectValue(ruleTypes, ruleMap) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - newRules = append(newRules, ruleTf) - } - - rulesTf, diags := types.ListValue(types.ObjectType{AttrTypes: ruleTypes}, newRules) - if diags.HasError() { - return core.DiagsToError(diags) - } - - model.Rules = rulesTf - return nil -} diff --git a/stackit/internal/services/observability/log-alertgroup/resource_test.go b/stackit/internal/services/observability/log-alertgroup/resource_test.go deleted file mode 100644 index 4f3bd60b..00000000 --- a/stackit/internal/services/observability/log-alertgroup/resource_test.go +++ /dev/null @@ -1,366 +0,0 @@ -package logalertgroup - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" -) - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - name string - input *Model - expect *observability.CreateLogsAlertgroupsPayload - expectErr bool - }{ - { - name: "Nil Model", - input: nil, - expect: nil, - expectErr: true, - }, - { - name: "Empty Model", - input: &Model{ - Name: types.StringNull(), - Interval: types.StringNull(), - Rules: types.ListNull(types.StringType), - }, - expect: &observability.CreateLogsAlertgroupsPayload{}, - expectErr: false, - }, - { - name: "Model with Name and Interval", - input: &Model{ - Name: types.StringValue("test-alertgroup"), - Interval: types.StringValue("5m"), - }, - expect: &observability.CreateLogsAlertgroupsPayload{ - Name: utils.Ptr("test-alertgroup"), - Interval: utils.Ptr("5m"), - }, - expectErr: false, - }, - { - name: "Model with Full Information", - input: &Model{ - Name: types.StringValue("full-alertgroup"), - Interval: types.StringValue("10m"), - Rules: types.ListValueMust( - types.ObjectType{AttrTypes: ruleTypes}, - []attr.Value{ - types.ObjectValueMust( - ruleTypes, - map[string]attr.Value{ - "alert": types.StringValue("alert"), - "expression": types.StringValue("expression"), - "for": types.StringValue("10s"), - "labels": types.MapValueMust( - types.StringType, - map[string]attr.Value{ - "k": types.StringValue("v"), - }, - ), - "annotations": types.MapValueMust( - types.StringType, - map[string]attr.Value{ - "k": types.StringValue("v"), - }, - ), - }, - ), - }, - ), - }, - expect: &observability.CreateLogsAlertgroupsPayload{ - Name: utils.Ptr("full-alertgroup"), - Interval: utils.Ptr("10m"), - Rules: &[]observability.UpdateAlertgroupsRequestInnerRulesInner{ - { - Alert: utils.Ptr("alert"), - Annotations: &map[string]interface{}{ - "k": "v", - }, - Expr: utils.Ptr("expression"), - For: utils.Ptr("10s"), - Labels: &map[string]interface{}{ - "k": "v", - }, - }, - }, - }, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toCreatePayload(ctx, tt.input) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err) - } - - if diff := cmp.Diff(got, tt.expect); diff != "" { - t.Errorf("unexpected result (-got +want):\n%s", diff) - } - }) - } -} - -func TestToRulesPayload(t *testing.T) { - tests := []struct { - name string - input *Model - expect []observability.UpdateAlertgroupsRequestInnerRulesInner - expectErr bool - }{ - { - name: "Nil Rules", - input: &Model{ - Rules: types.ListNull(types.StringType), // Simulates a lack of rules - }, - expect: []observability.UpdateAlertgroupsRequestInnerRulesInner{}, - expectErr: false, - }, - { - name: "Invalid Rule Element Type", - input: &Model{ - Rules: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("invalid"), // Should cause a conversion failure - }), - }, - expect: nil, - expectErr: true, - }, - { - name: "Single Valid Rule", - input: &Model{ - Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleTypes}, []attr.Value{ - types.ObjectValueMust(ruleTypes, map[string]attr.Value{ - "alert": types.StringValue("alert"), - "expression": types.StringValue("expr"), - "for": types.StringValue("5s"), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - "annotations": types.MapValueMust(types.StringType, map[string]attr.Value{ - "note": types.StringValue("important"), - }), - }), - }), - }, - expect: []observability.UpdateAlertgroupsRequestInnerRulesInner{ - { - Alert: utils.Ptr("alert"), - Expr: utils.Ptr("expr"), - For: utils.Ptr("5s"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Annotations: &map[string]interface{}{ - "note": "important", - }, - }, - }, - expectErr: false, - }, - { - name: "Multiple Valid Rules", - input: &Model{ - Rules: types.ListValueMust(types.ObjectType{AttrTypes: ruleTypes}, []attr.Value{ - types.ObjectValueMust(ruleTypes, map[string]attr.Value{ - "alert": types.StringValue("alert1"), - "expression": types.StringValue("expr1"), - "for": types.StringValue("5s"), - "labels": types.MapNull(types.StringType), - "annotations": types.MapNull(types.StringType), - }), - types.ObjectValueMust(ruleTypes, map[string]attr.Value{ - "alert": types.StringValue("alert2"), - "expression": types.StringValue("expr2"), - "for": types.StringValue("10s"), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "key": types.StringValue("value"), - }), - "annotations": types.MapValueMust(types.StringType, map[string]attr.Value{ - "note": types.StringValue("important"), - }), - }), - }), - }, - expect: []observability.UpdateAlertgroupsRequestInnerRulesInner{ - { - Alert: utils.Ptr("alert1"), - Expr: utils.Ptr("expr1"), - For: utils.Ptr("5s"), - }, - { - Alert: utils.Ptr("alert2"), - Expr: utils.Ptr("expr2"), - For: utils.Ptr("10s"), - Labels: &map[string]interface{}{ - "key": "value", - }, - Annotations: &map[string]interface{}{ - "note": "important", - }, - }, - }, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - got, err := toRulesPayload(ctx, tt.input) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err) - } - - if diff := cmp.Diff(got, tt.expect); diff != "" { - t.Errorf("unexpected result (-got +want):\n%s", diff) - } - }) - } -} - -func TestMapFields(t *testing.T) { - tests := []struct { - name string - alertGroup *observability.AlertGroup - model *Model - expectedName string - expectedID string - expectErr bool - }{ - { - name: "Nil AlertGroup", - alertGroup: nil, - model: &Model{}, - expectErr: true, - }, - { - name: "Nil Model", - alertGroup: &observability.AlertGroup{}, - model: nil, - expectErr: true, - }, - { - name: "Interval Missing", - alertGroup: &observability.AlertGroup{ - Name: utils.Ptr("alert-group-name"), - }, - model: &Model{ - Name: types.StringValue("alert-group-name"), - ProjectId: types.StringValue("project1"), - InstanceId: types.StringValue("instance1"), - }, - expectedName: "alert-group-name", - expectedID: "project1,instance1,alert-group-name", - expectErr: true, - }, - { - name: "Name Missing", - alertGroup: &observability.AlertGroup{ - Interval: utils.Ptr("5m"), - }, - model: &Model{ - Name: types.StringValue("model-name"), - InstanceId: types.StringValue("instance1"), - }, - expectErr: true, - }, - { - name: "Complete Model and AlertGroup", - alertGroup: &observability.AlertGroup{ - Name: utils.Ptr("alert-group-name"), - Interval: utils.Ptr("10m"), - }, - model: &Model{ - Name: types.StringValue("alert-group-name"), - ProjectId: types.StringValue("project1"), - InstanceId: types.StringValue("instance1"), - Id: types.StringValue("project1,instance1,alert-group-name"), - Interval: types.StringValue("10m"), - }, - expectedName: "alert-group-name", - expectedID: "project1,instance1,alert-group-name", - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - err := mapFields(ctx, tt.alertGroup, tt.model) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err) - } - - if !tt.expectErr { - if diff := cmp.Diff(tt.model.Name.ValueString(), tt.expectedName); diff != "" { - t.Errorf("unexpected name (-got +want):\n%s", diff) - } - if diff := cmp.Diff(tt.model.Id.ValueString(), tt.expectedID); diff != "" { - t.Errorf("unexpected ID (-got +want):\n%s", diff) - } - } - }) - } -} - -func TestMapRules(t *testing.T) { - tests := []struct { - name string - alertGroup *observability.AlertGroup - model *Model - expectErr bool - }{ - { - name: "Empty Rules", - alertGroup: &observability.AlertGroup{ - Rules: &[]observability.AlertRuleRecord{}, - }, - model: &Model{}, - expectErr: false, - }, - { - name: "Single Complete Rule", - alertGroup: &observability.AlertGroup{ - Rules: &[]observability.AlertRuleRecord{ - { - Alert: utils.Ptr("HighCPUUsage"), - Expr: utils.Ptr("rate(cpu_usage[5m]) > 0.9"), - For: utils.Ptr("2m"), - Labels: &map[string]string{"severity": "critical"}, - Annotations: &map[string]string{"summary": "CPU usage high"}, - Record: utils.Ptr("record1"), - }, - }, - }, - model: &Model{}, - expectErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - err := mapRules(ctx, tt.alertGroup, tt.model) - - if (err != nil) != tt.expectErr { - t.Fatalf("expected error: %v, got: %v", tt.expectErr, err != nil) - } - }) - } -} diff --git a/stackit/internal/services/observability/observability_acc_test.go b/stackit/internal/services/observability/observability_acc_test.go deleted file mode 100644 index 9e5393f7..00000000 --- a/stackit/internal/services/observability/observability_acc_test.go +++ /dev/null @@ -1,1073 +0,0 @@ -package observability_test - -import ( - "context" - _ "embed" - "fmt" - "maps" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" - - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" -) - -//go:embed testdata/resource-min.tf -var resourceMinConfig string - -//go:embed testdata/resource-max.tf -var resourceMaxConfig string - -// To prevent conversion issues -var alert_rule_expression = "sum(kube_pod_status_phase{phase=\"Running\"}) > 0" -var logalertgroup_expression = "sum(rate({namespace=\"example\"} |= \"Simulated error message\" [1m])) > 0" -var alert_rule_expression_updated = "sum(kube_pod_status_phase{phase=\"Error\"}) > 0" -var logalertgroup_expression_updated = "sum(rate({namespace=\"example\"} |= \"Another error message\" [1m])) > 0" - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "alertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-ag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "alert_rule_name": config.StringVariable("alert1"), - "alert_rule_expression": config.StringVariable(alert_rule_expression), - "instance_name": config.StringVariable(fmt.Sprintf("tf-acc-i%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "plan_name": config.StringVariable("Observability-Medium-EU01"), - "logalertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-lag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "logalertgroup_alert": config.StringVariable("alert1"), - "logalertgroup_expression": config.StringVariable(logalertgroup_expression), - "scrapeconfig_name": config.StringVariable(fmt.Sprintf("tf-acc-sc%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "scrapeconfig_metrics_path": config.StringVariable("/metrics"), - "scrapeconfig_targets_url": config.StringVariable("www.y97xyrrocx2gsxx.de"), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "alertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-ag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "alert_rule_name": config.StringVariable("alert1"), - "alert_rule_expression": config.StringVariable(alert_rule_expression), - "instance_name": config.StringVariable(fmt.Sprintf("tf-acc-i%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "plan_name": config.StringVariable("Observability-Medium-EU01"), - "logalertgroup_name": config.StringVariable(fmt.Sprintf("tf-acc-lag%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "logalertgroup_alert": config.StringVariable("alert1"), - "logalertgroup_expression": config.StringVariable(logalertgroup_expression), - "scrapeconfig_name": config.StringVariable(fmt.Sprintf("tf-acc-sc%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), - "scrapeconfig_metrics_path": config.StringVariable("/metrics"), - "scrapeconfig_targets_url_1": config.StringVariable("www.y97xyrrocx2gsxx.de"), - "scrapeconfig_targets_url_2": config.StringVariable("f6zkn8gzeigwanh.de"), - // alert group - "alert_for_time": config.StringVariable("60s"), - "alert_label": config.StringVariable("label1"), - "alert_annotation": config.StringVariable("annotation1"), - "alert_interval": config.StringVariable("5h"), - // max instance - "logs_retention_days": config.StringVariable("30"), - "traces_retention_days": config.StringVariable("30"), - "metrics_retention_days": config.StringVariable("90"), - "metrics_retention_days_5m_downsampling": config.StringVariable("90"), - "metrics_retention_days_1h_downsampling": config.StringVariable("90"), - "instance_acl_1": config.StringVariable("1.2.3.4/32"), - "instance_acl_2": config.StringVariable("111.222.111.222/32"), - "receiver_name": config.StringVariable("OpsGenieReceiverInfo"), - "auth_identity": config.StringVariable("aa@bb.ccc"), - "auth_password": config.StringVariable("password"), - "auth_username": config.StringVariable("username"), - "email_from": config.StringVariable("aa@bb.ccc"), - "email_send_resolved": config.StringVariable("true"), - "smart_host": config.StringVariable("smtp.gmail.com:587"), - "email_to": config.StringVariable("bb@bb.ccc"), - "opsgenie_api_key": config.StringVariable("example-api-key"), - "opsgenie_api_tags": config.StringVariable("observability-alert"), - "opsgenie_api_url": config.StringVariable("https://api.eu.opsgenie.com"), - "opsgenie_priority": config.StringVariable("P3"), - "opsgenie_send_resolved": config.StringVariable("false"), - "webhook_configs_url": config.StringVariable("https://example.com"), - "ms_teams": config.StringVariable("true"), - "google_chat": config.StringVariable("false"), - "webhook_configs_send_resolved": config.StringVariable("false"), - "group_by": config.StringVariable("alertname"), - "group_interval": config.StringVariable("10m"), - "group_wait": config.StringVariable("1m"), - "repeat_interval": config.StringVariable("1h"), - "resolve_timeout": config.StringVariable("5m"), - "smtp_auth_identity": config.StringVariable("aa@bb.ccc"), - "smtp_auth_password": config.StringVariable("password"), - "smtp_auth_username": config.StringVariable("username"), - "smtp_from": config.StringVariable("aa@bb.ccc"), - "smtp_smart_host": config.StringVariable("smtp.gmail.com:587"), - "match": config.StringVariable("alert1"), - "match_regex": config.StringVariable("alert1"), - "matchers": config.StringVariable("instance =~ \".*\""), - "continue": config.StringVariable("true"), - // credential - "credential_description": config.StringVariable("This is a description for the test credential."), - // logalertgroup - "logalertgroup_for_time": config.StringVariable("60s"), - "logalertgroup_label": config.StringVariable("label1"), - "logalertgroup_annotation": config.StringVariable("annotation1"), - "logalertgroup_interval": config.StringVariable("5h"), - // scrapeconfig - "scrapeconfig_label": config.StringVariable("label1"), - "scrapeconfig_interval": config.StringVariable("4m"), - "scrapeconfig_limit": config.StringVariable("7"), - "scrapeconfig_enable_url_params": config.StringVariable("false"), - "scrapeconfig_scheme": config.StringVariable("https"), - "scrapeconfig_timeout": config.StringVariable("2m"), - "scrapeconfig_auth_username": config.StringVariable("username"), - "scrapeconfig_auth_password": config.StringVariable("password"), -} - -func configVarsMinUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMin)) - maps.Copy(tempConfig, testConfigVarsMin) - tempConfig["alert_rule_name"] = config.StringVariable("alert1-updated") - return tempConfig -} - -func configVarsMaxUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMax)) - maps.Copy(tempConfig, testConfigVarsMax) - tempConfig["plan_name"] = config.StringVariable("Observability-Large-EU01") - tempConfig["alert_interval"] = config.StringVariable("1h") - tempConfig["alert_rule_expression"] = config.StringVariable(alert_rule_expression_updated) - tempConfig["logalertgroup_interval"] = config.StringVariable("1h") - tempConfig["logalertgroup_expression"] = config.StringVariable(logalertgroup_expression_updated) - tempConfig["webhook_configs_url"] = config.StringVariable("https://chat.googleapis.com/api") - tempConfig["ms_teams"] = config.StringVariable("false") - tempConfig["google_chat"] = config.StringVariable("true") - tempConfig["matchers"] = config.StringVariable("instance =~ \"my.*\"") - return tempConfig -} - -func TestAccResourceMin(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckObservabilityDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMin, - Config: testutil.ObservabilityProviderConfig() + resourceMinConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["instance_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMin["plan_name"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "is_updatable"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_public_read_access"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_user"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_password"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "targets_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "alerting_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_ui_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "otlp_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "zipkin_spans_url"), - - // scrape config data - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "project_id", - "stackit_observability_scrapeconfig.scrapeconfig", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "name", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_name"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.#", "1"), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "metrics_path", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_metrics_path"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.0", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_targets_url"])), - - // credentials - resource.TestCheckResourceAttr("stackit_observability_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_credential.credential", "instance_id", - ), - resource.TestCheckNoResourceAttr("stackit_observability_credential.credential", "description"), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "username"), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "password"), - - // alertgroup - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_alertgroup.alertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["alertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMin["alert_rule_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), - - // logalertgroup - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_logalertgroup.logalertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["logalertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMin["logalertgroup_alert"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.expression", logalertgroup_expression), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMin, - Config: fmt.Sprintf(` - %s - - data "stackit_observability_instance" "instance" { - project_id = stackit_observability_instance.instance.project_id - instance_id = stackit_observability_instance.instance.instance_id - } - - data "stackit_observability_scrapeconfig" "scrapeconfig" { - project_id = stackit_observability_scrapeconfig.scrapeconfig.project_id - instance_id = stackit_observability_scrapeconfig.scrapeconfig.instance_id - name = stackit_observability_scrapeconfig.scrapeconfig.name - } - - data "stackit_observability_alertgroup" "alertgroup" { - project_id = stackit_observability_alertgroup.alertgroup.project_id - instance_id = stackit_observability_alertgroup.alertgroup.instance_id - name = stackit_observability_alertgroup.alertgroup.name - } - - data "stackit_observability_logalertgroup" "logalertgroup" { - project_id = stackit_observability_logalertgroup.logalertgroup.project_id - instance_id = stackit_observability_logalertgroup.logalertgroup.instance_id - name = stackit_observability_logalertgroup.logalertgroup.name - } - `, - testutil.ObservabilityProviderConfig()+resourceMinConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["instance_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMin["plan_name"])), - - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "project_id", - "data.stackit_observability_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "data.stackit_observability_instance.instance", "instance_id", - ), - - // scrape config data - resource.TestCheckResourceAttrPair( - "stackit_observability_scrapeconfig.scrapeconfig", "project_id", - "data.stackit_observability_scrapeconfig.scrapeconfig", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - "data.stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_scrapeconfig.scrapeconfig", "name", - "data.stackit_observability_scrapeconfig.scrapeconfig", "name", - ), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "name", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.#", "1"), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "metrics_path", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_metrics_path"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.0", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_targets_url"])), - - // alertgroup - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_alertgroup.alertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["alertgroup_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMin["alert_rule_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), - - // logalertgroup - resource.TestCheckResourceAttr("data.stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_logalertgroup.logalertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("data.stackit_observability_logalertgroup.logalertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["logalertgroup_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_logalertgroup.logalertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMin["logalertgroup_alert"])), - resource.TestCheckResourceAttr("data.stackit_observability_logalertgroup.logalertgroup", "rules.0.expression", logalertgroup_expression), - ), - }, - // Import 1 - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_observability_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Import 2 - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_observability_scrapeconfig.scrapeconfig", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_scrapeconfig.scrapeconfig"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_scrapeconfig.scrapeconfig") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Import 3 - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_observability_alertgroup.alertgroup", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_alertgroup.alertgroup"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_alertgroup.alertgroup") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Import 4 - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_observability_logalertgroup.logalertgroup", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_logalertgroup.logalertgroup"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_logalertgroup.logalertgroup") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: configVarsMinUpdated(), - Config: testutil.ObservabilityProviderConfig() + resourceMinConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["instance_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMin["plan_name"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "is_updatable"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_public_read_access"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_user"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_password"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "targets_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "alerting_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_ui_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "otlp_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "zipkin_spans_url"), - - // scrape config data - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "project_id", - "stackit_observability_scrapeconfig.scrapeconfig", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "name", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_name"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.#", "1"), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "metrics_path", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_metrics_path"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.0", testutil.ConvertConfigVariable(testConfigVarsMin["scrapeconfig_targets_url"])), - - // credentials - resource.TestCheckResourceAttr("stackit_observability_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_credential.credential", "instance_id", - ), - resource.TestCheckNoResourceAttr("stackit_observability_credential.credential", "description"), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "username"), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "password"), - - // alertgroup - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_alertgroup.alertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["alertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(configVarsMinUpdated()["alert_rule_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), - - // logalertgroup - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_logalertgroup.logalertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMin["logalertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMin["logalertgroup_alert"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.expression", logalertgroup_expression), - ), - }, - }, - }) -} - -func TestAccResourceMax(t *testing.T) { - resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckObservabilityDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMax, - Config: testutil.ObservabilityProviderConfig() + resourceMaxConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["instance_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMax["plan_name"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "is_updatable"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_public_read_access"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_user"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_password"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "targets_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "alerting_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_ui_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "otlp_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "zipkin_spans_url"), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "logs_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["logs_retention_days"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "traces_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["traces_retention_days"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "metrics_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "metrics_retention_days_5m_downsampling", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days_5m_downsampling"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "metrics_retention_days_1h_downsampling", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days_1h_downsampling"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "acl.#", "2"), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["instance_acl_1"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "acl.1", testutil.ConvertConfigVariable(testConfigVarsMax["instance_acl_2"])), - - // alert config - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_identity", testutil.ConvertConfigVariable(testConfigVarsMax["auth_identity"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_password", testutil.ConvertConfigVariable(testConfigVarsMax["auth_password"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_username", testutil.ConvertConfigVariable(testConfigVarsMax["auth_username"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.from", testutil.ConvertConfigVariable(testConfigVarsMax["email_from"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.smart_host", testutil.ConvertConfigVariable(testConfigVarsMax["smart_host"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.to", testutil.ConvertConfigVariable(testConfigVarsMax["email_to"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.tags", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_tags"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.priority", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_priority"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.url", testutil.ConvertConfigVariable(testConfigVarsMax["webhook_configs_url"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.ms_teams", testutil.ConvertConfigVariable(testConfigVarsMax["ms_teams"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.google_chat", testutil.ConvertConfigVariable(testConfigVarsMax["google_chat"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.group_by.0", testutil.ConvertConfigVariable(testConfigVarsMax["group_by"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.group_interval", testutil.ConvertConfigVariable(testConfigVarsMax["group_interval"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.group_wait", testutil.ConvertConfigVariable(testConfigVarsMax["group_wait"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.receiver", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.group_by.0", testutil.ConvertConfigVariable(testConfigVarsMax["group_by"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.group_interval", testutil.ConvertConfigVariable(testConfigVarsMax["group_interval"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.group_wait", testutil.ConvertConfigVariable(testConfigVarsMax["group_wait"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.receiver", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.continue", testutil.ConvertConfigVariable(testConfigVarsMax["continue"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match.match1", testutil.ConvertConfigVariable(testConfigVarsMax["match"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match_regex.match_regex1", testutil.ConvertConfigVariable(testConfigVarsMax["match_regex"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.0", testutil.ConvertConfigVariable(testConfigVarsMax["matchers"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.#", "1"), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.resolve_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["resolve_timeout"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_auth_identity", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_auth_identity"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_auth_password", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_auth_password"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_auth_username", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_auth_username"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_from", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_from"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_smart_host", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_smart_host"])), - - // scrape config data - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "project_id", - "stackit_observability_scrapeconfig.scrapeconfig", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "name", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_name"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.#", "2"), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "metrics_path", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_metrics_path"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.0", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_targets_url_1"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.1", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_targets_url_2"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_label"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "scrape_interval", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_interval"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "sample_limit", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_limit"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "saml2.enable_url_parameters", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_enable_url_params"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "scheme", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_scheme"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "scrape_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_timeout"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "basic_auth.username", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_auth_username"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "basic_auth.password", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_auth_password"])), - - // credentials - resource.TestCheckResourceAttr("stackit_observability_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_credential.credential", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_credential.credential", "description", testutil.ConvertConfigVariable(testConfigVarsMax["credential_description"])), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "username"), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "password"), - - // alertgroup - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_alertgroup.alertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMax["alertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMax["alert_rule_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["alert_for_time"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), - - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_annotation"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "interval", testutil.ConvertConfigVariable(testConfigVarsMax["alert_interval"])), - - // logalertgroup - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_logalertgroup.logalertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_alert"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.expression", logalertgroup_expression), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_for_time"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_label"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_annotation"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "interval", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_interval"])), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMax, - Config: fmt.Sprintf(` - %s - - data "stackit_observability_instance" "instance" { - project_id = stackit_observability_instance.instance.project_id - instance_id = stackit_observability_instance.instance.instance_id - } - - data "stackit_observability_scrapeconfig" "scrapeconfig" { - project_id = stackit_observability_scrapeconfig.scrapeconfig.project_id - instance_id = stackit_observability_scrapeconfig.scrapeconfig.instance_id - name = stackit_observability_scrapeconfig.scrapeconfig.name - } - - data "stackit_observability_alertgroup" "alertgroup" { - project_id = stackit_observability_alertgroup.alertgroup.project_id - instance_id = stackit_observability_alertgroup.alertgroup.instance_id - name = stackit_observability_alertgroup.alertgroup.name - } - - data "stackit_observability_logalertgroup" "logalertgroup" { - project_id = stackit_observability_logalertgroup.logalertgroup.project_id - instance_id = stackit_observability_logalertgroup.logalertgroup.instance_id - name = stackit_observability_logalertgroup.logalertgroup.name - } - `, - testutil.ObservabilityProviderConfig()+resourceMaxConfig, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["instance_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "plan_name", testutil.ConvertConfigVariable(testConfigVarsMax["plan_name"])), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "is_updatable"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "grafana_public_read_access"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "grafana_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "grafana_initial_admin_user"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "grafana_initial_admin_password"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "metrics_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "metrics_push_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "targets_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "alerting_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "logs_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "logs_push_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "jaeger_traces_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "jaeger_ui_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "otlp_traces_url"), - resource.TestCheckResourceAttrSet("data.stackit_observability_instance.instance", "zipkin_spans_url"), - - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "logs_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["logs_retention_days"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "traces_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["traces_retention_days"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "metrics_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "metrics_retention_days_5m_downsampling", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days_5m_downsampling"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "metrics_retention_days_1h_downsampling", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days_1h_downsampling"])), - - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "acl.#", "2"), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["instance_acl_1"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "acl.1", testutil.ConvertConfigVariable(testConfigVarsMax["instance_acl_2"])), - // alert configdata. - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_identity", testutil.ConvertConfigVariable(testConfigVarsMax["auth_identity"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_password", testutil.ConvertConfigVariable(testConfigVarsMax["auth_password"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_username", testutil.ConvertConfigVariable(testConfigVarsMax["auth_username"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.from", testutil.ConvertConfigVariable(testConfigVarsMax["email_from"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.smart_host", testutil.ConvertConfigVariable(testConfigVarsMax["smart_host"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.to", testutil.ConvertConfigVariable(testConfigVarsMax["email_to"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.tags", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_tags"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.priority", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_priority"])), - - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.url", testutil.ConvertConfigVariable(testConfigVarsMax["webhook_configs_url"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.ms_teams", testutil.ConvertConfigVariable(testConfigVarsMax["ms_teams"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.google_chat", testutil.ConvertConfigVariable(testConfigVarsMax["google_chat"])), - - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.group_by.0", testutil.ConvertConfigVariable(testConfigVarsMax["group_by"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.group_interval", testutil.ConvertConfigVariable(testConfigVarsMax["group_interval"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.group_wait", testutil.ConvertConfigVariable(testConfigVarsMax["group_wait"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.receiver", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), - - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.group_by.0", testutil.ConvertConfigVariable(testConfigVarsMax["group_by"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.group_interval", testutil.ConvertConfigVariable(testConfigVarsMax["group_interval"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.group_wait", testutil.ConvertConfigVariable(testConfigVarsMax["group_wait"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.receiver", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.continue", testutil.ConvertConfigVariable(testConfigVarsMax["continue"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.match.match1", testutil.ConvertConfigVariable(testConfigVarsMax["match"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.match_regex.match_regex1", testutil.ConvertConfigVariable(testConfigVarsMax["match_regex"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.0", testutil.ConvertConfigVariable(testConfigVarsMax["matchers"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.#", "1"), - - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.global.opsgenie_api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.global.opsgenie_api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.global.resolve_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["resolve_timeout"])), - resource.TestCheckResourceAttr("data.stackit_observability_instance.instance", "alert_config.global.smtp_from", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_from"])), - - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "project_id", - "data.stackit_observability_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "data.stackit_observability_instance.instance", "instance_id", - ), - // scrape config data - resource.TestCheckResourceAttrPair( - "stackit_observability_scrapeconfig.scrapeconfig", "project_id", - "data.stackit_observability_scrapeconfig.scrapeconfig", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - "data.stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_scrapeconfig.scrapeconfig", "name", - "data.stackit_observability_scrapeconfig.scrapeconfig", "name", - ), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "name", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.#", "2"), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "metrics_path", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_metrics_path"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.0", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_targets_url_1"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.1", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_targets_url_2"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "targets.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_label"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "scrape_interval", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_interval"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "sample_limit", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_limit"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "saml2.enable_url_parameters", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_enable_url_params"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "scheme", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_scheme"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "scrape_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_timeout"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "basic_auth.username", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_auth_username"])), - resource.TestCheckResourceAttr("data.stackit_observability_scrapeconfig.scrapeconfig", "basic_auth.password", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_auth_password"])), - - // alertgroup - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "data.stackit_observability_alertgroup.alertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMax["alertgroup_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMax["alert_rule_name"])), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["alert_for_time"])), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_annotation"])), - resource.TestCheckResourceAttr("data.stackit_observability_alertgroup.alertgroup", "interval", testutil.ConvertConfigVariable(testConfigVarsMax["alert_interval"])), - - // logalertgroup - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "data.stackit_observability_logalertgroup.logalertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_alert"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.expression", logalertgroup_expression), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_for_time"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_label"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_annotation"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "interval", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_interval"])), - ), - }, - // Import 1 - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_observability_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"alert_config.global.smtp_auth_identity", "alert_config.global.smtp_auth_password", "alert_config.global.smtp_auth_username", "alert_config.global.smtp_smart_host"}, - }, - // Import 2 - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_observability_scrapeconfig.scrapeconfig", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_scrapeconfig.scrapeconfig"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_scrapeconfig.scrapeconfig") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"alert_config.global.smtp_auth_identity", "alert_config.global.smtp_auth_password", "alert_config.global.smtp_auth_username", "alert_config.global.smtp_smart_host"}, - }, - // Import 3 - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_observability_alertgroup.alertgroup", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_alertgroup.alertgroup"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_alertgroup.alertgroup") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"alert_config.global.smtp_auth_identity", "alert_config.global.smtp_auth_password", "alert_config.global.smtp_auth_username", "alert_config.global.smtp_smart_host"}, - }, - // Import 4 - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_observability_logalertgroup.logalertgroup", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_observability_logalertgroup.logalertgroup"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_observability_logalertgroup.logalertgroup") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"alert_config.global.smtp_auth_identity", "alert_config.global.smtp_auth_password", "alert_config.global.smtp_auth_username", "alert_config.global.smtp_smart_host"}, - }, - // Update - { - ConfigVariables: configVarsMaxUpdated(), - Config: testutil.ObservabilityProviderConfig() + resourceMaxConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["instance_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "plan_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["plan_name"])), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "dashboard_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "is_updatable"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_public_read_access"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_user"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "grafana_initial_admin_password"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "metrics_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "targets_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "alerting_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "logs_push_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "jaeger_ui_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "otlp_traces_url"), - resource.TestCheckResourceAttrSet("stackit_observability_instance.instance", "zipkin_spans_url"), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "logs_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["logs_retention_days"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "traces_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["traces_retention_days"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "metrics_retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "metrics_retention_days_5m_downsampling", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days_5m_downsampling"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "metrics_retention_days_1h_downsampling", testutil.ConvertConfigVariable(testConfigVarsMax["metrics_retention_days_1h_downsampling"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "acl.#", "2"), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["instance_acl_1"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "acl.1", testutil.ConvertConfigVariable(testConfigVarsMax["instance_acl_2"])), - - // alert config - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_identity", testutil.ConvertConfigVariable(testConfigVarsMax["auth_identity"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_password", testutil.ConvertConfigVariable(testConfigVarsMax["auth_password"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.auth_username", testutil.ConvertConfigVariable(testConfigVarsMax["auth_username"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.from", testutil.ConvertConfigVariable(testConfigVarsMax["email_from"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.smart_host", testutil.ConvertConfigVariable(testConfigVarsMax["smart_host"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.email_configs.0.to", testutil.ConvertConfigVariable(testConfigVarsMax["email_to"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.tags", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_tags"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.opsgenie_configs.0.priority", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_priority"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.url", testutil.ConvertConfigVariable(configVarsMaxUpdated()["webhook_configs_url"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.ms_teams", testutil.ConvertConfigVariable(configVarsMaxUpdated()["ms_teams"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.receivers.0.webhooks_configs.0.google_chat", testutil.ConvertConfigVariable(configVarsMaxUpdated()["google_chat"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.group_by.0", testutil.ConvertConfigVariable(testConfigVarsMax["group_by"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.group_interval", testutil.ConvertConfigVariable(testConfigVarsMax["group_interval"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.group_wait", testutil.ConvertConfigVariable(testConfigVarsMax["group_wait"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.receiver", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.group_by.0", testutil.ConvertConfigVariable(testConfigVarsMax["group_by"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.group_interval", testutil.ConvertConfigVariable(testConfigVarsMax["group_interval"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.group_wait", testutil.ConvertConfigVariable(testConfigVarsMax["group_wait"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.receiver", testutil.ConvertConfigVariable(testConfigVarsMax["receiver_name"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.repeat_interval", testutil.ConvertConfigVariable(testConfigVarsMax["repeat_interval"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.continue", testutil.ConvertConfigVariable(testConfigVarsMax["continue"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match.match1", testutil.ConvertConfigVariable(testConfigVarsMax["match"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.match_regex.match_regex1", testutil.ConvertConfigVariable(testConfigVarsMax["match_regex"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["matchers"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.route.routes.0.matchers.#", "1"), - - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_key", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_key"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.opsgenie_api_url", testutil.ConvertConfigVariable(testConfigVarsMax["opsgenie_api_url"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.resolve_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["resolve_timeout"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_auth_identity", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_auth_identity"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_auth_password", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_auth_password"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_auth_username", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_auth_username"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_from", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_from"])), - resource.TestCheckResourceAttr("stackit_observability_instance.instance", "alert_config.global.smtp_smart_host", testutil.ConvertConfigVariable(testConfigVarsMax["smtp_smart_host"])), - - // scrape config data - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "project_id", - "stackit_observability_scrapeconfig.scrapeconfig", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_scrapeconfig.scrapeconfig", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "name", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_name"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.#", "2"), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "metrics_path", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_metrics_path"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.0", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_targets_url_1"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.urls.1", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_targets_url_2"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "targets.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_label"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "scrape_interval", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_interval"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "sample_limit", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_limit"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "saml2.enable_url_parameters", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_enable_url_params"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "scheme", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_scheme"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "scrape_timeout", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_timeout"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "basic_auth.username", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_auth_username"])), - resource.TestCheckResourceAttr("stackit_observability_scrapeconfig.scrapeconfig", "basic_auth.password", testutil.ConvertConfigVariable(testConfigVarsMax["scrapeconfig_auth_password"])), - - // credentials - resource.TestCheckResourceAttr("stackit_observability_credential.credential", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_credential.credential", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_credential.credential", "description", testutil.ConvertConfigVariable(testConfigVarsMax["credential_description"])), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "username"), - resource.TestCheckResourceAttrSet("stackit_observability_credential.credential", "password"), - - // alertgroup - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_alertgroup.alertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMax["alertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMax["alert_rule_name"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.expression", alert_rule_expression_updated), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["alert_for_time"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_label"])), - - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["alert_annotation"])), - resource.TestCheckResourceAttr("stackit_observability_alertgroup.alertgroup", "interval", testutil.ConvertConfigVariable(configVarsMaxUpdated()["alert_interval"])), - - // logalertgroup - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_observability_instance.instance", "instance_id", - "stackit_observability_logalertgroup.logalertgroup", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "name", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_name"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.alert", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_alert"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.expression", logalertgroup_expression_updated), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.for", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_for_time"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.labels.label1", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_label"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "rules.0.annotations.annotation1", testutil.ConvertConfigVariable(testConfigVarsMax["logalertgroup_annotation"])), - resource.TestCheckResourceAttr("stackit_observability_logalertgroup.logalertgroup", "interval", testutil.ConvertConfigVariable(configVarsMaxUpdated()["logalertgroup_interval"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckObservabilityDestroy(s *terraform.State) error { - ctx := context.Background() - var client *observability.APIClient - var err error - if testutil.ObservabilityCustomEndpoint == "" { - client, err = observability.NewAPIClient( - stackitSdkConfig.WithRegion("eu01"), - ) - } else { - client, err = observability.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.ObservabilityCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_observability_instance" { - continue - } - // instance terraform ID: = "[project_id],[instance_id],[name]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := *instancesResp.Instances - for i := range instances { - if utils.Contains(instancesToDestroy, *instances[i].Id) { - if *instances[i].Status != observability.PROJECTINSTANCEFULLSTATUS_DELETE_SUCCEEDED { - _, err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].Id) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].Id, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].Id).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].Id, err) - } - } - } - } - return nil -} diff --git a/stackit/internal/services/observability/scrapeconfig/datasource.go b/stackit/internal/services/observability/scrapeconfig/datasource.go deleted file mode 100644 index 32d38a7b..00000000 --- a/stackit/internal/services/observability/scrapeconfig/datasource.go +++ /dev/null @@ -1,229 +0,0 @@ -package observability - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &scrapeConfigDataSource{} -) - -// NewScrapeConfigDataSource is a helper function to simplify the provider implementation. -func NewScrapeConfigDataSource() datasource.DataSource { - return &scrapeConfigDataSource{} -} - -// scrapeConfigDataSource is the data source implementation. -type scrapeConfigDataSource struct { - client *observability.APIClient -} - -// Metadata returns the data source type name. -func (d *scrapeConfigDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_scrapeconfig" -} - -func (d *scrapeConfigDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient -} - -// Schema defines the schema for the data source. -func (d *scrapeConfigDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability scrape config data source schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source. ID. It is structured as \"`project_id`,`instance_id`,`name`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the scraping job is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: "Observability instance ID to which the scraping job is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "Specifies the name of the scraping job", - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - stringvalidator.LengthBetween(1, 200), - }, - }, - "metrics_path": schema.StringAttribute{ - Description: "Specifies the job scraping url path.", - Computed: true, - }, - - "scheme": schema.StringAttribute{ - Description: "Specifies the http scheme.", - Computed: true, - }, - - "scrape_interval": schema.StringAttribute{ - Description: "Specifies the scrape interval as duration string.", - Validators: []validator.String{ - stringvalidator.LengthBetween(2, 8), - }, - Computed: true, - }, - - "sample_limit": schema.Int64Attribute{ - Description: "Specifies the scrape sample limit.", - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(1, 3000000), - }, - }, - - "scrape_timeout": schema.StringAttribute{ - Description: "Specifies the scrape timeout as duration string.", - Computed: true, - }, - "saml2": schema.SingleNestedAttribute{ - Description: "A SAML2 configuration block.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "enable_url_parameters": schema.BoolAttribute{ - Description: "Specifies if URL parameters are enabled", - Computed: true, - }, - }, - }, - "basic_auth": schema.SingleNestedAttribute{ - Description: "A basic authentication block.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "username": schema.StringAttribute{ - Description: "Specifies basic auth username.", - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 200), - }, - }, - "password": schema.StringAttribute{ - Description: "Specifies basic auth password.", - Computed: true, - Sensitive: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 200), - }, - }, - }, - }, - "targets": schema.ListNestedAttribute{ - Description: "The targets list (specified by the static config).", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "urls": schema.ListAttribute{ - Description: "Specifies target URLs.", - Computed: true, - ElementType: types.StringType, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - stringvalidator.LengthBetween(1, 500), - ), - }, - }, - "labels": schema.MapAttribute{ - Description: "Specifies labels.", - Computed: true, - ElementType: types.StringType, - Validators: []validator.Map{ - mapvalidator.SizeAtMost(10), - mapvalidator.ValueStringsAre(stringvalidator.LengthBetween(0, 200)), - mapvalidator.KeysAre(stringvalidator.LengthBetween(0, 200)), - }, - }, - }, - }, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *scrapeConfigDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - scName := model.Name.ValueString() - - scResp, err := d.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading scrape config", - fmt.Sprintf("Scrape config with name %q or instance with ID %q does not exist in project %q.", scName, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, scResp.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Observability scrape config read") -} diff --git a/stackit/internal/services/observability/scrapeconfig/resource.go b/stackit/internal/services/observability/scrapeconfig/resource.go deleted file mode 100644 index 2fc2e999..00000000 --- a/stackit/internal/services/observability/scrapeconfig/resource.go +++ /dev/null @@ -1,865 +0,0 @@ -package observability - -import ( - "context" - "fmt" - "net/http" - "strings" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "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/objectdefault" - "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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -const ( - DefaultScheme = observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTP // API default is "http" - DefaultScrapeInterval = "5m" - DefaultScrapeTimeout = "2m" - DefaultSampleLimit = int64(5000) - DefaultSAML2EnableURLParameters = true -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &scrapeConfigResource{} - _ resource.ResourceWithConfigure = &scrapeConfigResource{} - _ resource.ResourceWithImportState = &scrapeConfigResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - InstanceId types.String `tfsdk:"instance_id"` - Name types.String `tfsdk:"name"` - MetricsPath types.String `tfsdk:"metrics_path"` - Scheme types.String `tfsdk:"scheme"` - ScrapeInterval types.String `tfsdk:"scrape_interval"` - ScrapeTimeout types.String `tfsdk:"scrape_timeout"` - SampleLimit types.Int64 `tfsdk:"sample_limit"` - SAML2 types.Object `tfsdk:"saml2"` - BasicAuth types.Object `tfsdk:"basic_auth"` - Targets types.List `tfsdk:"targets"` -} - -// Struct corresponding to Model.SAML2 -type saml2Model struct { - EnableURLParameters types.Bool `tfsdk:"enable_url_parameters"` -} - -// Types corresponding to saml2Model -var saml2Types = map[string]attr.Type{ - "enable_url_parameters": types.BoolType, -} - -// Struct corresponding to Model.BasicAuth -type basicAuthModel struct { - Username types.String `tfsdk:"username"` - Password types.String `tfsdk:"password"` -} - -// Types corresponding to basicAuthModel -var basicAuthTypes = map[string]attr.Type{ - "username": types.StringType, - "password": types.StringType, -} - -// Struct corresponding to Model.Targets[i] -type targetModel struct { - URLs types.List `tfsdk:"urls"` - Labels types.Map `tfsdk:"labels"` -} - -// Types corresponding to targetModel -var targetTypes = map[string]attr.Type{ - "urls": types.ListType{ElemType: types.StringType}, - "labels": types.MapType{ElemType: types.StringType}, -} - -// NewScrapeConfigResource is a helper function to simplify the provider implementation. -func NewScrapeConfigResource() resource.Resource { - return &scrapeConfigResource{} -} - -// scrapeConfigResource is the resource implementation. -type scrapeConfigResource struct { - client *observability.APIClient -} - -// Metadata returns the resource type name. -func (r *scrapeConfigResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_observability_scrapeconfig" -} - -// Configure adds the provider configured client to the resource. -func (r *scrapeConfigResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Observability scrape config client configured") -} - -// Schema defines the schema for the resource. -func (r *scrapeConfigResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Observability scrape config resource schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`name`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the scraping job is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: "Observability instance ID to which the scraping job is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "name": schema.StringAttribute{ - Description: "Specifies the name of the scraping job.", - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - stringvalidator.LengthBetween(1, 200), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "metrics_path": schema.StringAttribute{ - Description: "Specifies the job scraping url path. E.g. `/metrics`.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 200), - }, - }, - - "scheme": schema.StringAttribute{ - Description: "Specifies the http scheme. Defaults to `https`.", - Optional: true, - Computed: true, - Default: stringdefault.StaticString(string(DefaultScheme)), - }, - "scrape_interval": schema.StringAttribute{ - Description: "Specifies the scrape interval as duration string. Defaults to `5m`.", - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(2, 8), - }, - Default: stringdefault.StaticString(DefaultScrapeInterval), - }, - "scrape_timeout": schema.StringAttribute{ - Description: "Specifies the scrape timeout as duration string. Defaults to `2m`.", - Optional: true, - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(2, 8), - }, - Default: stringdefault.StaticString(DefaultScrapeTimeout), - }, - "sample_limit": schema.Int64Attribute{ - Description: "Specifies the scrape sample limit. Upper limit depends on the service plan. Defaults to `5000`.", - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(1, 3000000), - }, - Default: int64default.StaticInt64(DefaultSampleLimit), - }, - "saml2": schema.SingleNestedAttribute{ - Description: "A SAML2 configuration block.", - Optional: true, - Computed: true, - Default: objectdefault.StaticValue( - types.ObjectValueMust( - map[string]attr.Type{ - "enable_url_parameters": types.BoolType, - }, - map[string]attr.Value{ - "enable_url_parameters": types.BoolValue(DefaultSAML2EnableURLParameters), - }, - ), - ), - Attributes: map[string]schema.Attribute{ - "enable_url_parameters": schema.BoolAttribute{ - Description: "Specifies if URL parameters are enabled. Defaults to `true`", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(DefaultSAML2EnableURLParameters), - }, - }, - }, - "basic_auth": schema.SingleNestedAttribute{ - Description: "A basic authentication block.", - Optional: true, - Computed: true, - Attributes: map[string]schema.Attribute{ - "username": schema.StringAttribute{ - Description: "Specifies basic auth username.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 200), - }, - }, - "password": schema.StringAttribute{ - Description: "Specifies basic auth password.", - Required: true, - Sensitive: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 200), - }, - }, - }, - }, - "targets": schema.ListNestedAttribute{ - Description: "The targets list (specified by the static config).", - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "urls": schema.ListAttribute{ - Description: "Specifies target URLs.", - Required: true, - ElementType: types.StringType, - Validators: []validator.List{ - listvalidator.ValueStringsAre( - stringvalidator.LengthBetween(1, 500), - ), - }, - }, - "labels": schema.MapAttribute{ - Description: "Specifies labels.", - Optional: true, - ElementType: types.StringType, - Validators: []validator.Map{ - mapvalidator.SizeAtMost(10), - mapvalidator.ValueStringsAre(stringvalidator.LengthBetween(0, 200)), - mapvalidator.KeysAre(stringvalidator.LengthBetween(0, 200)), - }, - }, - }, - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - scName := model.Name.ValueString() - - saml2Model := saml2Model{} - if !model.SAML2.IsNull() && !model.SAML2.IsUnknown() { - diags = model.SAML2.As(ctx, &saml2Model, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - basicAuthModel := basicAuthModel{} - if !model.BasicAuth.IsNull() && !model.BasicAuth.IsUnknown() { - diags = model.BasicAuth.As(ctx, &basicAuthModel, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - targetsModel := []targetModel{} - if !model.Targets.IsNull() && !model.Targets.IsUnknown() { - diags = model.Targets.ElementsAs(ctx, &targetsModel, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(ctx, &model, &saml2Model, &basicAuthModel, targetsModel) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Creating API payload: %v", err)) - return - } - _, err = r.client.CreateScrapeConfig(ctx, instanceId, projectId).CreateScrapeConfigPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.CreateScrapeConfigWaitHandler(ctx, r.client, instanceId, scName, projectId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Scrape config creation waiting: %v", err)) - return - } - got, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Calling API for updated data: %v", err)) - return - } - err = mapFields(ctx, got.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Observability scrape config created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *scrapeConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - scName := model.Name.ValueString() - - scResp, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading scrape config", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, scResp.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading scrape config", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed model - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Observability scrape config read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - scName := model.Name.ValueString() - - saml2Model := saml2Model{} - if !model.SAML2.IsNull() && !model.SAML2.IsUnknown() { - diags = model.SAML2.As(ctx, &saml2Model, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - basicAuthModel := basicAuthModel{} - if !model.BasicAuth.IsNull() && !model.BasicAuth.IsUnknown() { - diags = model.BasicAuth.As(ctx, &basicAuthModel, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - targetsModel := []targetModel{} - if !model.Targets.IsNull() && !model.Targets.IsUnknown() { - diags = model.Targets.ElementsAs(ctx, &targetsModel, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toUpdatePayload(ctx, &model, &saml2Model, &basicAuthModel, targetsModel) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Creating API payload: %v", err)) - return - } - _, err = r.client.UpdateScrapeConfig(ctx, instanceId, scName, projectId).UpdateScrapeConfigPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // We do not have an update status provided by the observability scrape config api, so we cannot use a waiter here, hence a simple sleep is used. - time.Sleep(15 * time.Second) - - // Fetch updated ScrapeConfig - scResp, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Calling API for updated data: %v", err)) - return - } - err = mapFields(ctx, scResp.Data, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Observability scrape config updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *scrapeConfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - scName := model.Name.ValueString() - - // Delete existing ScrapeConfig - _, err := r.client.DeleteScrapeConfig(ctx, instanceId, scName, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting scrape config", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteScrapeConfigWaitHandler(ctx, r.client, instanceId, scName, projectId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting scrape config", fmt.Sprintf("Scrape config deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Observability scrape config deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,name -func (r *scrapeConfigResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing scrape config", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id],[name] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "Observability scrape config state imported") -} - -func mapFields(ctx context.Context, sc *observability.Job, model *Model) error { - if sc == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var scName string - if model.Name.ValueString() != "" { - scName = model.Name.ValueString() - } else if sc.JobName != nil { - scName = *sc.JobName - } else { - return fmt.Errorf("scrape config name not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), scName) - model.Name = types.StringValue(scName) - model.MetricsPath = types.StringPointerValue(sc.MetricsPath) - model.Scheme = types.StringValue(string(sc.GetScheme())) - model.ScrapeInterval = types.StringPointerValue(sc.ScrapeInterval) - model.ScrapeTimeout = types.StringPointerValue(sc.ScrapeTimeout) - model.SampleLimit = types.Int64PointerValue(sc.SampleLimit) - err := mapSAML2(sc, model) - if err != nil { - return fmt.Errorf("map saml2: %w", err) - } - err = mapBasicAuth(sc, model) - if err != nil { - return fmt.Errorf("map basic auth: %w", err) - } - err = mapTargets(ctx, sc, model) - if err != nil { - return fmt.Errorf("map targets: %w", err) - } - return nil -} - -func mapBasicAuth(sc *observability.Job, model *Model) error { - if sc.BasicAuth == nil { - model.BasicAuth = types.ObjectNull(basicAuthTypes) - return nil - } - basicAuthMap := map[string]attr.Value{ - "username": types.StringValue(*sc.BasicAuth.Username), - "password": types.StringValue(*sc.BasicAuth.Password), - } - basicAuthTF, diags := types.ObjectValue(basicAuthTypes, basicAuthMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.BasicAuth = basicAuthTF - return nil -} - -func mapSAML2(sc *observability.Job, model *Model) error { - if (sc.Params == nil || *sc.Params == nil) && model.SAML2.IsNull() { - return nil - } - - if model.SAML2.IsNull() || model.SAML2.IsUnknown() { - model.SAML2 = types.ObjectNull(saml2Types) - } - - flag := true - if sc.Params == nil || *sc.Params == nil { - return nil - } - p := *sc.Params - if v, ok := p["saml2"]; ok { - if len(v) == 1 && v[0] == "disabled" { - flag = false - } - } - - saml2Map := map[string]attr.Value{ - "enable_url_parameters": types.BoolValue(flag), - } - saml2TF, diags := types.ObjectValue(saml2Types, saml2Map) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.SAML2 = saml2TF - return nil -} - -func mapTargets(ctx context.Context, sc *observability.Job, model *Model) error { - if sc == nil || sc.StaticConfigs == nil { - model.Targets = types.ListNull(types.ObjectType{AttrTypes: targetTypes}) - return nil - } - - targetsModel := []targetModel{} - if !model.Targets.IsNull() && !model.Targets.IsUnknown() { - diags := model.Targets.ElementsAs(ctx, &targetsModel, false) - if diags.HasError() { - return core.DiagsToError(diags) - } - } - - newTargets := []attr.Value{} - for i, sc := range *sc.StaticConfigs { - nt := targetModel{} - - // Map URLs - urls := []attr.Value{} - if sc.Targets != nil { - for _, v := range *sc.Targets { - urls = append(urls, types.StringValue(v)) - } - } - nt.URLs = types.ListValueMust(types.StringType, urls) - - // Map Labels - if len(model.Targets.Elements()) > i && targetsModel[i].Labels.IsNull() || sc.Labels == nil { - nt.Labels = types.MapNull(types.StringType) - } else { - newl := map[string]attr.Value{} - for k, v := range *sc.Labels { - newl[k] = types.StringValue(v) - } - nt.Labels = types.MapValueMust(types.StringType, newl) - } - - // Build target - targetMap := map[string]attr.Value{ - "urls": nt.URLs, - "labels": nt.Labels, - } - targetTF, diags := types.ObjectValue(targetTypes, targetMap) - if diags.HasError() { - return core.DiagsToError(diags) - } - - newTargets = append(newTargets, targetTF) - } - - targetsTF, diags := types.ListValue(types.ObjectType{AttrTypes: targetTypes}, newTargets) - if diags.HasError() { - return core.DiagsToError(diags) - } - - model.Targets = targetsTF - return nil -} - -func toCreatePayload(ctx context.Context, model *Model, saml2Model *saml2Model, basicAuthModel *basicAuthModel, targetsModel []targetModel) (*observability.CreateScrapeConfigPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - sc := observability.CreateScrapeConfigPayload{ - JobName: conversion.StringValueToPointer(model.Name), - MetricsPath: conversion.StringValueToPointer(model.MetricsPath), - ScrapeInterval: conversion.StringValueToPointer(model.ScrapeInterval), - ScrapeTimeout: conversion.StringValueToPointer(model.ScrapeTimeout), - // potentially lossy conversion, depending on the allowed range for sample_limit - SampleLimit: sdkUtils.Ptr(float64(model.SampleLimit.ValueInt64())), - Scheme: observability.CreateScrapeConfigPayloadGetSchemeAttributeType(conversion.StringValueToPointer(model.Scheme)), - } - setDefaultsCreateScrapeConfig(&sc, model, saml2Model) - - if !saml2Model.EnableURLParameters.IsNull() && !saml2Model.EnableURLParameters.IsUnknown() { - m := make(map[string]interface{}) - if sc.Params != nil { - m = *sc.Params - } - if saml2Model.EnableURLParameters.ValueBool() { - m["saml2"] = []string{"enabled"} - } else { - m["saml2"] = []string{"disabled"} - } - sc.Params = &m - } - - if sc.BasicAuth == nil && !basicAuthModel.Username.IsNull() && !basicAuthModel.Password.IsNull() { - sc.BasicAuth = &observability.CreateScrapeConfigPayloadBasicAuth{ - Username: conversion.StringValueToPointer(basicAuthModel.Username), - Password: conversion.StringValueToPointer(basicAuthModel.Password), - } - } - - t := make([]observability.CreateScrapeConfigPayloadStaticConfigsInner, len(targetsModel)) - for i, target := range targetsModel { - ti := observability.CreateScrapeConfigPayloadStaticConfigsInner{} - - urls := []string{} - diags := target.URLs.ElementsAs(ctx, &urls, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - ti.Targets = &urls - - labels := map[string]interface{}{} - for k, v := range target.Labels.Elements() { - labels[k], _ = conversion.ToString(ctx, v) - } - ti.Labels = &labels - t[i] = ti - } - sc.StaticConfigs = &t - - return &sc, nil -} - -func setDefaultsCreateScrapeConfig(sc *observability.CreateScrapeConfigPayload, model *Model, saml2Model *saml2Model) { - if sc == nil { - return - } - if model.Scheme.IsNull() || model.Scheme.IsUnknown() { - sc.Scheme = DefaultScheme.Ptr() - } - if model.ScrapeInterval.IsNull() || model.ScrapeInterval.IsUnknown() { - sc.ScrapeInterval = sdkUtils.Ptr(DefaultScrapeInterval) - } - if model.ScrapeTimeout.IsNull() || model.ScrapeTimeout.IsUnknown() { - sc.ScrapeTimeout = sdkUtils.Ptr(DefaultScrapeTimeout) - } - if model.SampleLimit.IsNull() || model.SampleLimit.IsUnknown() { - sc.SampleLimit = sdkUtils.Ptr(float64(DefaultSampleLimit)) - } - // Make the API default more explicit by setting the field. - if saml2Model.EnableURLParameters.IsNull() || saml2Model.EnableURLParameters.IsUnknown() { - m := map[string]interface{}{} - if sc.Params != nil { - m = *sc.Params - } - if DefaultSAML2EnableURLParameters { - m["saml2"] = []string{"enabled"} - } else { - m["saml2"] = []string{"disabled"} - } - sc.Params = &m - } -} - -func toUpdatePayload(ctx context.Context, model *Model, saml2Model *saml2Model, basicAuthModel *basicAuthModel, targetsModel []targetModel) (*observability.UpdateScrapeConfigPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - sc := observability.UpdateScrapeConfigPayload{ - MetricsPath: conversion.StringValueToPointer(model.MetricsPath), - ScrapeInterval: conversion.StringValueToPointer(model.ScrapeInterval), - ScrapeTimeout: conversion.StringValueToPointer(model.ScrapeTimeout), - // potentially lossy conversion, depending on the allowed range for sample_limit - SampleLimit: sdkUtils.Ptr(float64(model.SampleLimit.ValueInt64())), - Scheme: observability.UpdateScrapeConfigPayloadGetSchemeAttributeType(conversion.StringValueToPointer(model.Scheme)), - } - setDefaultsUpdateScrapeConfig(&sc, model) - - if !saml2Model.EnableURLParameters.IsNull() && !saml2Model.EnableURLParameters.IsUnknown() { - m := make(map[string]interface{}) - if sc.Params != nil { - m = *sc.Params - } - if saml2Model.EnableURLParameters.ValueBool() { - m["saml2"] = []string{"enabled"} - } else { - m["saml2"] = []string{"disabled"} - } - sc.Params = &m - } - - if sc.BasicAuth == nil && !basicAuthModel.Username.IsNull() && !basicAuthModel.Password.IsNull() { - sc.BasicAuth = &observability.CreateScrapeConfigPayloadBasicAuth{ - Username: conversion.StringValueToPointer(basicAuthModel.Username), - Password: conversion.StringValueToPointer(basicAuthModel.Password), - } - } - - t := make([]observability.UpdateScrapeConfigPayloadStaticConfigsInner, len(targetsModel)) - for i, target := range targetsModel { - ti := observability.UpdateScrapeConfigPayloadStaticConfigsInner{} - - urls := []string{} - diags := target.URLs.ElementsAs(ctx, &urls, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - ti.Targets = &urls - - ls := map[string]interface{}{} - for k, v := range target.Labels.Elements() { - ls[k], _ = conversion.ToString(ctx, v) - } - ti.Labels = &ls - t[i] = ti - } - sc.StaticConfigs = &t - - return &sc, nil -} - -func setDefaultsUpdateScrapeConfig(sc *observability.UpdateScrapeConfigPayload, model *Model) { - if sc == nil { - return - } - if model.Scheme.IsNull() || model.Scheme.IsUnknown() { - sc.Scheme = observability.UpdateScrapeConfigPayloadGetSchemeAttributeType(DefaultScheme.Ptr()) - } - if model.ScrapeInterval.IsNull() || model.ScrapeInterval.IsUnknown() { - sc.ScrapeInterval = sdkUtils.Ptr(DefaultScrapeInterval) - } - if model.ScrapeTimeout.IsNull() || model.ScrapeTimeout.IsUnknown() { - sc.ScrapeTimeout = sdkUtils.Ptr(DefaultScrapeTimeout) - } - if model.SampleLimit.IsNull() || model.SampleLimit.IsUnknown() { - sc.SampleLimit = sdkUtils.Ptr(float64(DefaultSampleLimit)) - } -} diff --git a/stackit/internal/services/observability/scrapeconfig/resource_test.go b/stackit/internal/services/observability/scrapeconfig/resource_test.go deleted file mode 100644 index 3ef068c1..00000000 --- a/stackit/internal/services/observability/scrapeconfig/resource_test.go +++ /dev/null @@ -1,504 +0,0 @@ -package observability - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *observability.Job - expected Model - isValid bool - }{ - { - "default_ok", - &observability.Job{ - JobName: utils.Ptr("name"), - }, - Model{ - Id: types.StringValue("pid,iid,name"), - ProjectId: types.StringValue("pid"), - InstanceId: types.StringValue("iid"), - Name: types.StringValue("name"), - MetricsPath: types.StringNull(), - Scheme: types.StringValue(""), - ScrapeInterval: types.StringNull(), - ScrapeTimeout: types.StringNull(), - SAML2: types.ObjectNull(saml2Types), - BasicAuth: types.ObjectNull(basicAuthTypes), - Targets: types.ListNull(types.ObjectType{AttrTypes: targetTypes}), - }, - true, - }, - { - description: "values_ok", - input: &observability.Job{ - JobName: utils.Ptr("name"), - MetricsPath: utils.Ptr("/m"), - BasicAuth: &observability.BasicAuth{ - Password: utils.Ptr("p"), - Username: utils.Ptr("u"), - }, - Params: &map[string][]string{"saml2": {"disabled"}, "x": {"y", "z"}}, - Scheme: observability.JOBSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("1"), - ScrapeTimeout: utils.Ptr("2"), - SampleLimit: utils.Ptr(int64(17)), - StaticConfigs: &[]observability.StaticConfigs{ - { - Labels: &map[string]string{"k1": "v1"}, - Targets: &[]string{"url1"}, - }, - { - Labels: &map[string]string{"k2": "v2", "k3": "v3"}, - Targets: &[]string{"url1", "url3"}, - }, - { - Labels: nil, - Targets: &[]string{}, - }, - }, - }, - expected: Model{ - Id: types.StringValue("pid,iid,name"), - ProjectId: types.StringValue("pid"), - InstanceId: types.StringValue("iid"), - Name: types.StringValue("name"), - MetricsPath: types.StringValue("/m"), - Scheme: types.StringValue(string(observability.JOBSCHEME_HTTP)), - ScrapeInterval: types.StringValue("1"), - ScrapeTimeout: types.StringValue("2"), - SampleLimit: types.Int64Value(17), - SAML2: types.ObjectValueMust(saml2Types, map[string]attr.Value{ - "enable_url_parameters": types.BoolValue(false), - }), - BasicAuth: types.ObjectValueMust(basicAuthTypes, map[string]attr.Value{ - "username": types.StringValue("u"), - "password": types.StringValue("p"), - }), - Targets: types.ListValueMust(types.ObjectType{AttrTypes: targetTypes}, []attr.Value{ - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "urls": types.ListValueMust(types.StringType, []attr.Value{types.StringValue("url1")}), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "k1": types.StringValue("v1"), - }), - }), - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "urls": types.ListValueMust(types.StringType, []attr.Value{types.StringValue("url1"), types.StringValue("url3")}), - "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ - "k2": types.StringValue("v2"), - "k3": types.StringValue("v3"), - }), - }), - types.ObjectValueMust(targetTypes, map[string]attr.Value{ - "urls": types.ListValueMust(types.StringType, []attr.Value{}), - "labels": types.MapNull(types.StringType), - }), - }), - }, - isValid: true, - }, - { - "response_nil_fail", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &observability.Job{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(context.Background(), tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputSAML2 *saml2Model - inputBasicAuth *basicAuthModel - inputTargets []targetModel - expected *observability.CreateScrapeConfigPayload - isValid bool - }{ - { - "basic_ok", - &Model{ - MetricsPath: types.StringValue("/metrics"), - }, - &saml2Model{}, - &basicAuthModel{}, - []targetModel{}, - &observability.CreateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - // Defaults - Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.CreateScrapeConfigPayloadStaticConfigsInner{}, - Params: &map[string]any{"saml2": []string{"enabled"}}, - }, - true, - }, - { - "ok - false enable_url_parameters", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Name: types.StringValue("Name"), - }, - &saml2Model{ - EnableURLParameters: types.BoolValue(false), - }, - &basicAuthModel{}, - []targetModel{}, - &observability.CreateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - JobName: utils.Ptr("Name"), - Params: &map[string]any{"saml2": []string{"disabled"}}, - // Defaults - Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.CreateScrapeConfigPayloadStaticConfigsInner{}, - }, - true, - }, - { - "ok - true enable_url_parameters", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Name: types.StringValue("Name"), - }, - &saml2Model{ - EnableURLParameters: types.BoolValue(true), - }, - &basicAuthModel{}, - []targetModel{}, - &observability.CreateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - JobName: utils.Ptr("Name"), - Params: &map[string]any{"saml2": []string{"enabled"}}, - // Defaults - Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.CreateScrapeConfigPayloadStaticConfigsInner{}, - }, - true, - }, - { - "ok - with basic auth", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Name: types.StringValue("Name"), - }, - &saml2Model{}, - &basicAuthModel{ - Username: types.StringValue("u"), - Password: types.StringValue("p"), - }, - []targetModel{}, - &observability.CreateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - JobName: utils.Ptr("Name"), - BasicAuth: &observability.CreateScrapeConfigPayloadBasicAuth{ - Username: utils.Ptr("u"), - Password: utils.Ptr("p"), - }, - // Defaults - Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.CreateScrapeConfigPayloadStaticConfigsInner{}, - Params: &map[string]any{"saml2": []string{"enabled"}}, - }, - true, - }, - { - "ok - with targets", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Name: types.StringValue("Name"), - }, - &saml2Model{}, - &basicAuthModel{}, - []targetModel{ - { - URLs: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("url1")}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"k1": types.StringValue("v1")}), - }, - { - URLs: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("url1"), types.StringValue("url3")}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"k2": types.StringValue("v2"), "k3": types.StringValue("v3")}), - }, - { - URLs: types.ListValueMust(types.StringType, []attr.Value{}), - Labels: types.MapNull(types.StringType), - }, - }, - &observability.CreateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - JobName: utils.Ptr("Name"), - StaticConfigs: &[]observability.CreateScrapeConfigPayloadStaticConfigsInner{ - { - Targets: &[]string{"url1"}, - Labels: &map[string]interface{}{"k1": "v1"}, - }, - { - Targets: &[]string{"url1", "url3"}, - Labels: &map[string]interface{}{"k2": "v2", "k3": "v3"}, - }, - { - Targets: &[]string{}, - Labels: &map[string]interface{}{}, - }, - }, - // Defaults - Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - Params: &map[string]any{"saml2": []string{"enabled"}}, - }, - true, - }, - { - "nil_model", - nil, - nil, - nil, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(context.Background(), tt.input, tt.inputSAML2, tt.inputBasicAuth, tt.inputTargets) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputSAML2 *saml2Model - basicAuthModel *basicAuthModel - inputTargets []targetModel - expected *observability.UpdateScrapeConfigPayload - isValid bool - }{ - { - "basic_ok", - &Model{ - MetricsPath: types.StringValue("/metrics"), - }, - &saml2Model{}, - &basicAuthModel{}, - []targetModel{}, - &observability.UpdateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - // Defaults - Scheme: observability.UPDATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.UpdateScrapeConfigPayloadStaticConfigsInner{}, - }, - true, - }, - { - "ok - true enable_url_parameters", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Scheme: types.StringValue("http"), - }, - &saml2Model{ - EnableURLParameters: types.BoolValue(true), - }, - &basicAuthModel{}, - []targetModel{}, - &observability.UpdateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - // Defaults - Scheme: observability.UPDATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.UpdateScrapeConfigPayloadStaticConfigsInner{}, - Params: &map[string]any{"saml2": []string{"enabled"}}, - }, - true, - }, - { - "ok - false enable_url_parameters", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Scheme: types.StringValue("http"), - }, - &saml2Model{ - EnableURLParameters: types.BoolValue(false), - }, - &basicAuthModel{}, - []targetModel{}, - &observability.UpdateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - // Defaults - Scheme: observability.UPDATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.UpdateScrapeConfigPayloadStaticConfigsInner{}, - Params: &map[string]any{"saml2": []string{"disabled"}}, - }, - true, - }, - { - "ok - with basic auth", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Name: types.StringValue("Name"), - }, - &saml2Model{}, - &basicAuthModel{ - Username: types.StringValue("u"), - Password: types.StringValue("p"), - }, - []targetModel{}, - &observability.UpdateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - BasicAuth: &observability.CreateScrapeConfigPayloadBasicAuth{ - Username: utils.Ptr("u"), - Password: utils.Ptr("p"), - }, - // Defaults - Scheme: observability.UPDATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - StaticConfigs: &[]observability.UpdateScrapeConfigPayloadStaticConfigsInner{}, - }, - true, - }, - { - "ok - with targets", - &Model{ - MetricsPath: types.StringValue("/metrics"), - Name: types.StringValue("Name"), - }, - &saml2Model{}, - &basicAuthModel{}, - []targetModel{ - { - URLs: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("url1")}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"k1": types.StringValue("v1")}), - }, - { - URLs: types.ListValueMust(types.StringType, []attr.Value{types.StringValue("url1"), types.StringValue("url3")}), - Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"k2": types.StringValue("v2"), "k3": types.StringValue("v3")}), - }, - { - URLs: types.ListValueMust(types.StringType, []attr.Value{}), - Labels: types.MapNull(types.StringType), - }, - }, - &observability.UpdateScrapeConfigPayload{ - MetricsPath: utils.Ptr("/metrics"), - StaticConfigs: &[]observability.UpdateScrapeConfigPayloadStaticConfigsInner{ - { - Targets: &[]string{"url1"}, - Labels: &map[string]interface{}{"k1": "v1"}, - }, - { - Targets: &[]string{"url1", "url3"}, - Labels: &map[string]interface{}{"k2": "v2", "k3": "v3"}, - }, - { - Targets: &[]string{}, - Labels: &map[string]interface{}{}, - }, - }, - // Defaults - Scheme: observability.UPDATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - SampleLimit: utils.Ptr(float64(5000)), - }, - true, - }, - { - "nil_model", - nil, - nil, - nil, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(context.Background(), tt.input, tt.inputSAML2, tt.basicAuthModel, tt.inputTargets) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/observability/testdata/resource-max.tf b/stackit/internal/services/observability/testdata/resource-max.tf deleted file mode 100644 index 28460222..00000000 --- a/stackit/internal/services/observability/testdata/resource-max.tf +++ /dev/null @@ -1,234 +0,0 @@ - -variable "project_id" {} - -variable "alertgroup_name" {} -variable "alert_rule_name" {} -variable "alert_rule_expression" {} -variable "alert_for_time" {} -variable "alert_label" {} -variable "alert_annotation" {} -variable "alert_interval" {} - -variable "instance_name" {} -variable "plan_name" {} -variable "logs_retention_days" {} -variable "traces_retention_days" {} -variable "metrics_retention_days" {} -variable "metrics_retention_days_5m_downsampling" {} -variable "metrics_retention_days_1h_downsampling" {} -variable "instance_acl_1" {} -variable "instance_acl_2" {} -variable "receiver_name" {} -variable "auth_identity" {} -variable "auth_password" {} -variable "auth_username" {} -variable "email_from" {} -variable "email_send_resolved" {} -variable "smart_host" {} -variable "email_to" {} -variable "opsgenie_api_key" {} -variable "opsgenie_api_tags" {} -variable "opsgenie_api_url" {} -variable "opsgenie_priority" {} -variable "opsgenie_send_resolved" {} -variable "webhook_configs_url" {} -variable "ms_teams" {} -variable "google_chat" {} -variable "webhook_configs_send_resolved" {} -variable "group_by" {} -variable "group_interval" {} -variable "group_wait" {} -variable "repeat_interval" {} -variable "resolve_timeout" {} -variable "smtp_auth_identity" {} -variable "smtp_auth_password" {} -variable "smtp_auth_username" {} -variable "smtp_from" {} -variable "smtp_smart_host" {} -variable "match" {} -variable "match_regex" {} -variable "matchers" {} -variable "continue" {} - -variable "credential_description" {} - -variable "logalertgroup_name" {} -variable "logalertgroup_alert" {} -variable "logalertgroup_expression" {} -variable "logalertgroup_for_time" {} -variable "logalertgroup_label" {} -variable "logalertgroup_annotation" {} -variable "logalertgroup_interval" {} - -variable "scrapeconfig_name" {} -variable "scrapeconfig_metrics_path" {} -variable "scrapeconfig_targets_url_1" {} -variable "scrapeconfig_targets_url_2" {} -variable "scrapeconfig_label" {} -variable "scrapeconfig_interval" {} -variable "scrapeconfig_limit" {} -variable "scrapeconfig_enable_url_params" {} -variable "scrapeconfig_scheme" {} -variable "scrapeconfig_timeout" {} -variable "scrapeconfig_auth_username" {} -variable "scrapeconfig_auth_password" {} - -resource "stackit_observability_alertgroup" "alertgroup" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id - name = var.alertgroup_name - rules = [ - { - alert = var.alert_rule_name - expression = var.alert_rule_expression - for = var.alert_for_time - labels = { - label1 = var.alert_label - }, - annotations = { - annotation1 = var.alert_annotation - } - } - ] - interval = var.alert_interval -} - -resource "stackit_observability_credential" "credential" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id - description = var.credential_description -} - -resource "stackit_observability_instance" "instance" { - project_id = var.project_id - name = var.instance_name - plan_name = var.plan_name - - logs_retention_days = var.logs_retention_days - traces_retention_days = var.traces_retention_days - metrics_retention_days = var.metrics_retention_days - metrics_retention_days_5m_downsampling = var.metrics_retention_days_5m_downsampling - metrics_retention_days_1h_downsampling = var.metrics_retention_days_1h_downsampling - acl = [var.instance_acl_1, var.instance_acl_2] - - // alert config - alert_config = { - receivers = [ - { - name = var.receiver_name - email_configs = [ - { - auth_identity = var.auth_identity - auth_password = var.auth_password - auth_username = var.auth_username - from = var.email_from - smart_host = var.smart_host - to = var.email_to - send_resolved = var.email_send_resolved - } - ] - opsgenie_configs = [ - { - api_key = var.opsgenie_api_key - tags = var.opsgenie_api_tags - api_url = var.opsgenie_api_url - priority = var.opsgenie_priority - send_resolved = var.opsgenie_send_resolved - } - ] - webhooks_configs = [ - { - url = var.webhook_configs_url - ms_teams = var.ms_teams - google_chat = var.google_chat - send_resolved = var.webhook_configs_send_resolved - } - ] - }, - ], - - route = { - group_by = [var.group_by] - group_interval = var.group_interval - group_wait = var.group_wait - receiver = var.receiver_name - repeat_interval = var.repeat_interval - routes = [ - { - group_by = [var.group_by] - group_interval = var.group_interval - group_wait = var.group_wait - receiver = var.receiver_name - repeat_interval = var.repeat_interval - continue = var.continue - match = { - match1 = var.match - } - match_regex = { - match_regex1 = var.match_regex - } - matchers = [ - var.matchers - ] - } - ] - }, - - global = { - opsgenie_api_key = var.opsgenie_api_key - opsgenie_api_url = var.opsgenie_api_url - resolve_timeout = var.resolve_timeout - smtp_auth_identity = var.smtp_auth_identity - smtp_auth_password = var.smtp_auth_password - smtp_auth_username = var.smtp_auth_username - smtp_from = var.smtp_from - smtp_smart_host = var.smtp_smart_host - } - } -} - -resource "stackit_observability_logalertgroup" "logalertgroup" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id - name = var.logalertgroup_name - rules = [ - { - alert = var.logalertgroup_alert - expression = var.logalertgroup_expression - for = var.logalertgroup_for_time - labels = { - label1 = var.logalertgroup_label - }, - annotations = { - annotation1 = var.logalertgroup_annotation - } - } - ] - interval = var.logalertgroup_interval -} - -resource "stackit_observability_scrapeconfig" "scrapeconfig" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id - name = var.scrapeconfig_name - metrics_path = var.scrapeconfig_metrics_path - - targets = [{ - urls = [var.scrapeconfig_targets_url_1, var.scrapeconfig_targets_url_2] - labels = { - label1 = var.scrapeconfig_label - } - }] - scheme = var.scrapeconfig_scheme - scrape_timeout = var.scrapeconfig_timeout - basic_auth = { - username = var.scrapeconfig_auth_username - password = var.scrapeconfig_auth_password - } - scrape_interval = var.scrapeconfig_interval - sample_limit = var.scrapeconfig_limit - saml2 = { - enable_url_parameters = var.scrapeconfig_enable_url_params - } - -} diff --git a/stackit/internal/services/observability/testdata/resource-min.tf b/stackit/internal/services/observability/testdata/resource-min.tf deleted file mode 100644 index 68d405f0..00000000 --- a/stackit/internal/services/observability/testdata/resource-min.tf +++ /dev/null @@ -1,69 +0,0 @@ - -variable "project_id" {} - -variable "alertgroup_name" {} -variable "alert_rule_name" {} -variable "alert_rule_expression" {} - -variable "instance_name" {} -variable "plan_name" {} - -variable "logalertgroup_name" {} -variable "logalertgroup_alert" {} -variable "logalertgroup_expression" {} - - -variable "scrapeconfig_name" {} -variable "scrapeconfig_metrics_path" {} -variable "scrapeconfig_targets_url" {} - - -resource "stackit_observability_alertgroup" "alertgroup" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id - name = var.alertgroup_name - rules = [ - { - alert = var.alert_rule_name - expression = var.alert_rule_expression - } - ] -} - -resource "stackit_observability_credential" "credential" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id -} - - -resource "stackit_observability_instance" "instance" { - project_id = var.project_id - name = var.instance_name - plan_name = var.plan_name -} - -resource "stackit_observability_logalertgroup" "logalertgroup" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id - name = var.logalertgroup_name - rules = [ - { - alert = var.logalertgroup_alert - expression = var.logalertgroup_expression - } - ] -} - - -resource "stackit_observability_scrapeconfig" "scrapeconfig" { - project_id = var.project_id - instance_id = stackit_observability_instance.instance.instance_id - name = var.scrapeconfig_name - metrics_path = var.scrapeconfig_metrics_path - - targets = [{ urls = [var.scrapeconfig_targets_url] }] -} - - - - diff --git a/stackit/internal/services/observability/utils/util.go b/stackit/internal/services/observability/utils/util.go deleted file mode 100644 index 3e549ce7..00000000 --- a/stackit/internal/services/observability/utils/util.go +++ /dev/null @@ -1,32 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/stackitcloud/stackit-sdk-go/services/observability" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *observability.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ObservabilityCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ObservabilityCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := observability.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/observability/utils/util_test.go b/stackit/internal/services/observability/utils/util_test.go deleted file mode 100644 index 88386fb8..00000000 --- a/stackit/internal/services/observability/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://observability-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *observability.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *observability.APIClient { - apiClient, err := observability.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ObservabilityCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *observability.APIClient { - apiClient, err := observability.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/opensearch/credential/datasource.go b/stackit/internal/services/opensearch/credential/datasource.go deleted file mode 100644 index 86350f60..00000000 --- a/stackit/internal/services/opensearch/credential/datasource.go +++ /dev/null @@ -1,177 +0,0 @@ -package opensearch - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &credentialDataSource{} -) - -// NewCredentialDataSource is a helper function to simplify the provider implementation. -func NewCredentialDataSource() datasource.DataSource { - return &credentialDataSource{} -} - -// credentialDataSource is the data source implementation. -type credentialDataSource struct { - client *opensearch.APIClient -} - -// Metadata returns the data source type name. -func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_opensearch_credential" -} - -// Configure adds the provider configured client to the data source. -func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "OpenSearch credential client configured") -} - -// Schema defines the schema for the data source. -func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "OpenSearch credential data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the OpenSearch instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "scheme": schema.StringAttribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading credential", - fmt.Sprintf("Credential with ID %q or instance with ID %q does not exist in project %q.", credentialId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "OpenSearch credential read") -} diff --git a/stackit/internal/services/opensearch/credential/resource.go b/stackit/internal/services/opensearch/credential/resource.go deleted file mode 100644 index 041718dd..00000000 --- a/stackit/internal/services/opensearch/credential/resource.go +++ /dev/null @@ -1,372 +0,0 @@ -package opensearch - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialResource{} - _ resource.ResourceWithConfigure = &credentialResource{} - _ resource.ResourceWithImportState = &credentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialId types.String `tfsdk:"credential_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Host types.String `tfsdk:"host"` - Hosts types.List `tfsdk:"hosts"` - Password types.String `tfsdk:"password"` - Port types.Int64 `tfsdk:"port"` - Scheme types.String `tfsdk:"scheme"` - Uri types.String `tfsdk:"uri"` - Username types.String `tfsdk:"username"` -} - -// NewCredentialResource is a helper function to simplify the provider implementation. -func NewCredentialResource() resource.Resource { - return &credentialResource{} -} - -// credentialResource is the resource implementation. -type credentialResource struct { - client *opensearch.APIClient -} - -// Metadata returns the resource type name. -func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_opensearch_credential" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "OpenSearch credential client configured") -} - -// Schema defines the schema for the resource. -func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "OpenSearch credential resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the OpenSearch instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "scheme": schema.StringAttribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Create new recordset - credentialsResp, err := r.client.CreateCredentials(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if credentialsResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") - return - } - credentialId := *credentialsResp.Id - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "OpenSearch credential created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "OpenSearch credential read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - // Delete existing record set - err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "OpenSearch credential deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id -func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...) - tflog.Info(ctx, "OpenSearch credential state imported") -} - -func mapFields(ctx context.Context, credentialsResp *opensearch.CredentialsResponse, model *Model) error { - if credentialsResp == nil { - return fmt.Errorf("response input is nil") - } - if credentialsResp.Raw == nil { - return fmt.Errorf("response credentials raw is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - credentials := credentialsResp.Raw.Credentials - - var credentialId string - if model.CredentialId.ValueString() != "" { - credentialId = model.CredentialId.ValueString() - } else if credentialsResp.Id != nil { - credentialId = *credentialsResp.Id - } else { - return fmt.Errorf("credentials id not present") - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId, - ) - - modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) - if err != nil { - return err - } - - model.CredentialId = types.StringValue(credentialId) - model.Hosts = types.ListNull(types.StringType) - if credentials != nil { - if credentials.Hosts != nil { - respHosts := *credentials.Hosts - reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) - - hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) - if diags.HasError() { - return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) - } - - model.Hosts = hostsTF - } - model.Host = types.StringPointerValue(credentials.Host) - model.Password = types.StringPointerValue(credentials.Password) - model.Port = types.Int64PointerValue(credentials.Port) - model.Scheme = types.StringPointerValue(credentials.Scheme) - model.Uri = types.StringPointerValue(credentials.Uri) - model.Username = types.StringPointerValue(credentials.Username) - } - return nil -} diff --git a/stackit/internal/services/opensearch/credential/resource_test.go b/stackit/internal/services/opensearch/credential/resource_test.go deleted file mode 100644 index baab0252..00000000 --- a/stackit/internal/services/opensearch/credential/resource_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package opensearch - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *opensearch.CredentialsResponse - expected Model - isValid bool - }{ - { - "default_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &opensearch.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &opensearch.RawCredentials{}, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringNull(), - Hosts: types.ListNull(types.StringType), - Password: types.StringNull(), - Port: types.Int64Null(), - Scheme: types.StringNull(), - Uri: types.StringNull(), - Username: types.StringNull(), - }, - true, - }, - { - "simple_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &opensearch.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &opensearch.RawCredentials{ - Credentials: &opensearch.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "host_1", - "", - }, - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Scheme: utils.Ptr("scheme"), - Uri: utils.Ptr("uri"), - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_1"), - types.StringValue(""), - }), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Scheme: types.StringValue("scheme"), - Uri: types.StringValue("uri"), - Username: types.StringValue("username"), - }, - true, - }, - { - "hosts_unordered", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - }, - &opensearch.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &opensearch.RawCredentials{ - Credentials: &opensearch.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "", - "host_1", - "host_2", - }, - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Scheme: utils.Ptr("scheme"), - Uri: utils.Ptr("uri"), - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Scheme: types.StringValue("scheme"), - Uri: types.StringValue("uri"), - Username: types.StringValue("username"), - }, - true, - }, - { - "null_fields_and_int_conversions", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &opensearch.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &opensearch.RawCredentials{ - Credentials: &opensearch.Credentials{ - Host: utils.Ptr(""), - Hosts: &[]string{}, - Password: utils.Ptr(""), - Port: utils.Ptr(int64(2123456789)), - Scheme: nil, - Uri: nil, - Username: utils.Ptr(""), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue(""), - Hosts: types.ListValueMust(types.StringType, []attr.Value{}), - Password: types.StringValue(""), - Port: types.Int64Value(2123456789), - Scheme: types.StringNull(), - Uri: types.StringNull(), - Username: types.StringValue(""), - }, - true, - }, - { - "nil_response", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &opensearch.CredentialsResponse{}, - Model{}, - false, - }, - { - "nil_raw_credential", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &opensearch.CredentialsResponse{ - Id: utils.Ptr("cid"), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/opensearch/instance/datasource.go b/stackit/internal/services/opensearch/instance/datasource.go deleted file mode 100644 index 6e5a2595..00000000 --- a/stackit/internal/services/opensearch/instance/datasource.go +++ /dev/null @@ -1,265 +0,0 @@ -package opensearch - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *opensearch.APIClient -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_opensearch_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "OpenSearch instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "OpenSearch instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the OpenSearch instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "enable_monitoring": "Enable monitoring.", - "graphite": "If set, monitoring with Graphite will be enabled. Expects the host and port where the Graphite metrics should be sent to (host:port).", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted (in seconds).", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "java_garbage_collector": "The garbage collector to use for OpenSearch.", - "java_heapspace": "The amount of memory (in MB) allocated as heap by the JVM for OpenSearch.", - "java_maxmetaspace": "The amount of memory (in MB) used by the JVM to store metadata for OpenSearch.", - "plugins": "List of plugins to install. Must be a supported plugin name. The plugins `repository-s3` and `repository-azure` are enabled by default and cannot be disabled.", - "syslog": "List of syslog servers to send logs to.", - "tls_ciphers": "List of TLS ciphers to use.", - "tls_protocols": "The TLS protocol to use.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Computed: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Computed: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Computed: true, - }, - "java_garbage_collector": schema.StringAttribute{ - Description: parametersDescriptions["java_garbage_collector"], - Computed: true, - }, - "java_heapspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_heapspace"], - Computed: true, - }, - "java_maxmetaspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_maxmetaspace"], - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Computed: true, - }, - "plugins": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["plugins"], - Computed: true, - }, - "syslog": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["syslog"], - Computed: true, - }, - "tls_ciphers": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["tls_ciphers"], - Computed: true, - }, - "tls_protocols": schema.StringAttribute{ - Description: parametersDescriptions["tls_protocols"], - Computed: true, - }, - }, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - }, - "image_url": schema.StringAttribute{ - Computed: true, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - http.StatusGone: fmt.Sprintf("Instance %q is gone.", instanceId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "OpenSearch instance read") -} diff --git a/stackit/internal/services/opensearch/instance/resource.go b/stackit/internal/services/opensearch/instance/resource.go deleted file mode 100644 index e2355495..00000000 --- a/stackit/internal/services/opensearch/instance/resource.go +++ /dev/null @@ -1,840 +0,0 @@ -package opensearch - -import ( - "context" - "fmt" - "net/http" - "slices" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - CfGuid types.String `tfsdk:"cf_guid"` - CfSpaceGuid types.String `tfsdk:"cf_space_guid"` - DashboardUrl types.String `tfsdk:"dashboard_url"` - ImageUrl types.String `tfsdk:"image_url"` - Name types.String `tfsdk:"name"` - CfOrganizationGuid types.String `tfsdk:"cf_organization_guid"` - Parameters types.Object `tfsdk:"parameters"` - Version types.String `tfsdk:"version"` - PlanName types.String `tfsdk:"plan_name"` - PlanId types.String `tfsdk:"plan_id"` -} - -// Struct corresponding to DataSourceModel.Parameters -type parametersModel struct { - SgwAcl types.String `tfsdk:"sgw_acl"` - EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` - Graphite types.String `tfsdk:"graphite"` - JavaGarbageCollector types.String `tfsdk:"java_garbage_collector"` - JavaHeapspace types.Int64 `tfsdk:"java_heapspace"` - JavaMaxmetaspace types.Int64 `tfsdk:"java_maxmetaspace"` - MaxDiskThreshold types.Int64 `tfsdk:"max_disk_threshold"` - MetricsFrequency types.Int64 `tfsdk:"metrics_frequency"` - MetricsPrefix types.String `tfsdk:"metrics_prefix"` - MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"` - Plugins types.List `tfsdk:"plugins"` - Syslog types.List `tfsdk:"syslog"` - TlsCiphers types.List `tfsdk:"tls_ciphers"` - TlsProtocols types.List `tfsdk:"tls_protocols"` -} - -// Types corresponding to parametersModel -var parametersTypes = map[string]attr.Type{ - "sgw_acl": basetypes.StringType{}, - "enable_monitoring": basetypes.BoolType{}, - "graphite": basetypes.StringType{}, - "java_garbage_collector": basetypes.StringType{}, - "java_heapspace": basetypes.Int64Type{}, - "java_maxmetaspace": basetypes.Int64Type{}, - "max_disk_threshold": basetypes.Int64Type{}, - "metrics_frequency": basetypes.Int64Type{}, - "metrics_prefix": basetypes.StringType{}, - "monitoring_instance_id": basetypes.StringType{}, - "plugins": basetypes.ListType{ElemType: types.StringType}, - "syslog": basetypes.ListType{ElemType: types.StringType}, - "tls_ciphers": basetypes.ListType{ElemType: types.StringType}, - "tls_protocols": basetypes.ListType{ElemType: types.StringType}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *opensearch.APIClient -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_opensearch_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "OpenSearch instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "OpenSearch instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the OpenSearch instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - "parameters": "Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "enable_monitoring": "Enable monitoring.", - "graphite": "If set, monitoring with Graphite will be enabled. Expects the host and port where the Graphite metrics should be sent to (host:port).", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted (in seconds).", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key.", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "java_garbage_collector": "The garbage collector to use for OpenSearch.", - "java_heapspace": "The amount of memory (in MB) allocated as heap by the JVM for OpenSearch.", - "java_maxmetaspace": "The amount of memory (in MB) used by the JVM to store metadata for OpenSearch.", - "plugins": "List of plugins to install. Must be a supported plugin name. The plugins `repository-s3` and `repository-azure` are enabled by default and cannot be disabled.", - "syslog": "List of syslog servers to send logs to.", - "tls_ciphers": "List of TLS ciphers to use.", - "tls_protocols": "The TLS protocol to use.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Required: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Required: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Description: descriptions["parameters"], - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Optional: true, - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Optional: true, - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Optional: true, - Computed: true, - }, - "java_garbage_collector": schema.StringAttribute{ - Description: parametersDescriptions["java_garbage_collector"], - Optional: true, - Computed: true, - }, - "java_heapspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_heapspace"], - Optional: true, - Computed: true, - }, - "java_maxmetaspace": schema.Int64Attribute{ - Description: parametersDescriptions["java_maxmetaspace"], - Optional: true, - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Optional: true, - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Optional: true, - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Optional: true, - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Optional: true, - Computed: true, - }, - "plugins": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["plugins"], - Optional: true, - Computed: true, - }, - "syslog": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["syslog"], - Optional: true, - Computed: true, - }, - "tls_ciphers": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["tls_ciphers"], - Optional: true, - Computed: true, - }, - "tls_protocols": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["tls_protocols"], - Optional: true, - Computed: true, - }, - }, - Optional: true, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "image_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.InstanceId - ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "OpenSearch instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "OpenSearch instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - err = r.client.PartialUpdateInstance(ctx, projectId, instanceId).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "OpenSearch instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "OpenSearch instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "OpenSearch instance state imported") -} - -func mapFields(instance *opensearch.Instance, model *Model) error { - if instance == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.InstanceId != nil { - instanceId = *instance.InstanceId - } else { - return fmt.Errorf("instance id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) - model.InstanceId = types.StringValue(instanceId) - model.PlanId = types.StringPointerValue(instance.PlanId) - model.CfGuid = types.StringPointerValue(instance.CfGuid) - model.CfSpaceGuid = types.StringPointerValue(instance.CfSpaceGuid) - model.DashboardUrl = types.StringPointerValue(instance.DashboardUrl) - model.ImageUrl = types.StringPointerValue(instance.ImageUrl) - model.Name = types.StringPointerValue(instance.Name) - model.CfOrganizationGuid = types.StringPointerValue(instance.CfOrganizationGuid) - - if instance.Parameters == nil { - model.Parameters = types.ObjectNull(parametersTypes) - } else { - parameters, err := mapParameters(*instance.Parameters) - if err != nil { - return fmt.Errorf("mapping parameters: %w", err) - } - model.Parameters = parameters - } - return nil -} - -func mapParameters(params map[string]interface{}) (types.Object, error) { - attributes := map[string]attr.Value{} - for attribute := range parametersTypes { - var valueInterface interface{} - var ok bool - - // This replacement is necessary because Terraform does not allow hyphens in attribute names - // And the API uses hyphens in some of the attribute names, which would cause a mismatch - // The following attributes have hyphens in the API but underscores in the schema - hyphenAttributes := []string{ - "tls_ciphers", - "tls_protocols", - } - if slices.Contains(hyphenAttributes, attribute) { - alteredAttribute := strings.ReplaceAll(attribute, "_", "-") - valueInterface, ok = params[alteredAttribute] - } else { - valueInterface, ok = params[attribute] - } - if !ok { - // All fields are optional, so this is ok - // Set the value as nil, will be handled accordingly - valueInterface = nil - } - - var value attr.Value - switch parametersTypes[attribute].(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) - case basetypes.StringType: - if valueInterface == nil { - value = types.StringNull() - } else { - valueString, ok := valueInterface.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) - } - value = types.StringValue(valueString) - } - case basetypes.BoolType: - if valueInterface == nil { - value = types.BoolNull() - } else { - valueBool, ok := valueInterface.(bool) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) - } - value = types.BoolValue(valueBool) - } - case basetypes.Int64Type: - if valueInterface == nil { - value = types.Int64Null() - } else { - // This may be int64, int32, int or float64 - // We try to assert all 4 - var valueInt64 int64 - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) - case int64: - valueInt64 = temp - case int32: - valueInt64 = int64(temp) - case int: - valueInt64 = int64(temp) - case float64: - valueInt64 = int64(temp) - } - value = types.Int64Value(valueInt64) - } - case basetypes.ListType: // Assumed to be a list of strings - if valueInterface == nil { - value = types.ListNull(types.StringType) - } else { - // This may be []string{} or []interface{} - // We try to assert all 2 - var valueList []attr.Value - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) - case []string: - for _, x := range temp { - valueList = append(valueList, types.StringValue(x)) - } - case []interface{}: - for _, x := range temp { - xString, ok := x.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) - } - valueList = append(valueList, types.StringValue(xString)) - } - } - temp2, diags := types.ListValue(types.StringType, valueList) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) - } - value = temp2 - } - } - attributes[attribute] = value - } - - output, diags := types.ObjectValue(parametersTypes, attributes) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) - } - return output, nil -} - -func toCreatePayload(model *Model, parameters *parametersModel) (*opensearch.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("convert parameters: %w", err) - } - return &opensearch.CreateInstancePayload{ - InstanceName: conversion.StringValueToPointer(model.Name), - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toUpdatePayload(model *Model, parameters *parametersModel) (*opensearch.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("convert parameters: %w", err) - } - return &opensearch.PartialUpdateInstancePayload{ - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toInstanceParams(parameters *parametersModel) (*opensearch.InstanceParameters, error) { - if parameters == nil { - return nil, nil - } - payloadParams := &opensearch.InstanceParameters{} - - payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) - payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring) - payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite) - payloadParams.JavaGarbageCollector = opensearch.InstanceParametersGetJavaGarbageCollectorAttributeType(conversion.StringValueToPointer(parameters.JavaGarbageCollector)) - payloadParams.JavaHeapspace = conversion.Int64ValueToPointer(parameters.JavaHeapspace) - payloadParams.JavaMaxmetaspace = conversion.Int64ValueToPointer(parameters.JavaMaxmetaspace) - payloadParams.MaxDiskThreshold = conversion.Int64ValueToPointer(parameters.MaxDiskThreshold) - payloadParams.MetricsFrequency = conversion.Int64ValueToPointer(parameters.MetricsFrequency) - payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix) - payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) - - var err error - payloadParams.Plugins, err = conversion.StringListToPointer(parameters.Plugins) - if err != nil { - return nil, fmt.Errorf("convert plugins: %w", err) - } - - payloadParams.Syslog, err = conversion.StringListToPointer(parameters.Syslog) - if err != nil { - return nil, fmt.Errorf("convert syslog: %w", err) - } - - payloadParams.TlsCiphers, err = conversion.StringListToPointer(parameters.TlsCiphers) - if err != nil { - return nil, fmt.Errorf("convert tls_ciphers: %w", err) - } - - payloadParams.TlsProtocols, err = conversion.StringListToPointer(parameters.TlsProtocols) - if err != nil { - return nil, fmt.Errorf("convert tls_protocols: %w", err) - } - - return payloadParams, nil -} - -func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { - projectId := model.ProjectId.ValueString() - res, err := r.client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting OpenSearch offerings: %w", err) - } - - version := model.Version.ValueString() - planName := model.PlanName.ValueString() - availableVersions := "" - availablePlanNames := "" - isValidVersion := false - for _, offer := range *res.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) - continue - } - isValidVersion = true - - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue - } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - model.PlanId = types.StringPointerValue(plan.Id) - return nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) - } - } - - if !isValidVersion { - return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) - } - return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) -} - -func loadPlanNameAndVersion(ctx context.Context, client *opensearch.APIClient, model *Model) error { - projectId := model.ProjectId.ValueString() - planId := model.PlanId.ValueString() - res, err := client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting OpenSearch offerings: %w", err) - } - - for _, offer := range *res.Offerings { - for _, plan := range *offer.Plans { - if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { - model.PlanName = types.StringPointerValue(plan.Name) - model.Version = types.StringPointerValue(offer.Version) - return nil - } - } - } - - return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) -} diff --git a/stackit/internal/services/opensearch/instance/resource_test.go b/stackit/internal/services/opensearch/instance/resource_test.go deleted file mode 100644 index 82dd9b4d..00000000 --- a/stackit/internal/services/opensearch/instance/resource_test.go +++ /dev/null @@ -1,373 +0,0 @@ -package opensearch - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" -) - -var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringValue("acl"), - "enable_monitoring": types.BoolValue(true), - "graphite": types.StringValue("graphite"), - "java_garbage_collector": types.StringValue(string(opensearch.INSTANCEPARAMETERSJAVA_GARBAGE_COLLECTOR_USE_G1_GC)), - "java_heapspace": types.Int64Value(10), - "java_maxmetaspace": types.Int64Value(10), - "max_disk_threshold": types.Int64Value(10), - "metrics_frequency": types.Int64Value(10), - "metrics_prefix": types.StringValue("prefix"), - "monitoring_instance_id": types.StringValue("mid"), - "plugins": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("plugin"), - types.StringValue("plugin2"), - }), - "syslog": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("syslog"), - types.StringValue("syslog2"), - }), - "tls_ciphers": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("cipher"), - types.StringValue("cipher2"), - }), - "tls_protocols": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("TLSv1.2"), - types.StringValue("TLSv1.3"), - }), -}) - -var fixtureNullModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringNull(), - "enable_monitoring": types.BoolNull(), - "graphite": types.StringNull(), - "java_garbage_collector": types.StringNull(), - "java_heapspace": types.Int64Null(), - "java_maxmetaspace": types.Int64Null(), - "max_disk_threshold": types.Int64Null(), - "metrics_frequency": types.Int64Null(), - "metrics_prefix": types.StringNull(), - "monitoring_instance_id": types.StringNull(), - "plugins": types.ListNull(types.StringType), - "syslog": types.ListNull(types.StringType), - "tls_ciphers": types.ListNull(types.StringType), - "tls_protocols": types.ListNull(types.StringType), -}) - -var fixtureInstanceParameters = opensearch.InstanceParameters{ - SgwAcl: utils.Ptr("acl"), - EnableMonitoring: utils.Ptr(true), - Graphite: utils.Ptr("graphite"), - JavaGarbageCollector: opensearch.INSTANCEPARAMETERSJAVA_GARBAGE_COLLECTOR_USE_G1_GC.Ptr(), - JavaHeapspace: utils.Ptr(int64(10)), - JavaMaxmetaspace: utils.Ptr(int64(10)), - MaxDiskThreshold: utils.Ptr(int64(10)), - MetricsFrequency: utils.Ptr(int64(10)), - MetricsPrefix: utils.Ptr("prefix"), - MonitoringInstanceId: utils.Ptr("mid"), - Plugins: &[]string{"plugin", "plugin2"}, - Syslog: &[]string{"syslog", "syslog2"}, - TlsCiphers: &[]string{"cipher", "cipher2"}, - TlsProtocols: &[]string{"TLSv1.2", "TLSv1.3"}, -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *opensearch.Instance - expected Model - isValid bool - }{ - { - "default_values", - &opensearch.Instance{}, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringNull(), - Name: types.StringNull(), - CfGuid: types.StringNull(), - CfSpaceGuid: types.StringNull(), - DashboardUrl: types.StringNull(), - ImageUrl: types.StringNull(), - CfOrganizationGuid: types.StringNull(), - Parameters: types.ObjectNull(parametersTypes), - }, - true, - }, - { - "simple_values", - &opensearch.Instance{ - PlanId: utils.Ptr("plan"), - CfGuid: utils.Ptr("cf"), - CfSpaceGuid: utils.Ptr("space"), - DashboardUrl: utils.Ptr("dashboard"), - ImageUrl: utils.Ptr("image"), - InstanceId: utils.Ptr("iid"), - Name: utils.Ptr("name"), - CfOrganizationGuid: utils.Ptr("org"), - Parameters: &map[string]interface{}{ - // Using "-" on purpose on some fields because that is the API response - "sgw_acl": "acl", - "enable_monitoring": true, - "graphite": "graphite", - "java_garbage_collector": string(opensearch.INSTANCEPARAMETERSJAVA_GARBAGE_COLLECTOR_USE_G1_GC), - "java_heapspace": int64(10), - "java_maxmetaspace": int64(10), - "max_disk_threshold": int64(10), - "metrics_frequency": int64(10), - "metrics_prefix": "prefix", - "monitoring_instance_id": "mid", - "plugins": []string{"plugin", "plugin2"}, - "syslog": []string{"syslog", "syslog2"}, - "tls-ciphers": []string{"cipher", "cipher2"}, - "tls-protocols": []string{"TLSv1.2", "TLSv1.3"}, - }, - }, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringValue("plan"), - Name: types.StringValue("name"), - CfGuid: types.StringValue("cf"), - CfSpaceGuid: types.StringValue("space"), - DashboardUrl: types.StringValue("dashboard"), - ImageUrl: types.StringValue("image"), - CfOrganizationGuid: types.StringValue("org"), - Parameters: fixtureModelParameters, - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &opensearch.Instance{}, - Model{}, - false, - }, - { - "wrong_param_types_1", - &opensearch.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": true, - }, - }, - Model{}, - false, - }, - { - "wrong_param_types_2", - &opensearch.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": 1, - }, - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *opensearch.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &opensearch.CreateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &opensearch.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - PlanId: types.StringValue(""), - Parameters: fixtureNullModelParameters, - }, - &opensearch.CreateInstancePayload{ - InstanceName: utils.Ptr(""), - Parameters: &opensearch.InstanceParameters{}, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - }, - &opensearch.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toCreatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *opensearch.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &opensearch.PartialUpdateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &opensearch.PartialUpdateInstancePayload{ - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - PlanId: types.StringValue(""), - Parameters: fixtureNullModelParameters, - }, - &opensearch.PartialUpdateInstancePayload{ - Parameters: &opensearch.InstanceParameters{}, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - PlanId: types.StringValue("plan"), - }, - &opensearch.PartialUpdateInstancePayload{ - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toUpdatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/opensearch/opensearch_acc_test.go b/stackit/internal/services/opensearch/opensearch_acc_test.go deleted file mode 100644 index 223b12ce..00000000 --- a/stackit/internal/services/opensearch/opensearch_acc_test.go +++ /dev/null @@ -1,304 +0,0 @@ -package opensearch_test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// Instance resource data -var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": testutil.ResourceNameWithDateTime("opensearch"), - "plan_id": "9e4eac4b-b03d-4d7b-b01b-6d1224aa2d68", - "plan_name": "stackit-opensearch-1.2.10-replica", - "version": "2", - "sgw_acl-1": "192.168.0.0/16", - "sgw_acl-2": "192.168.0.0/24", - "max_disk_threshold": "80", - "enable_monitoring": "false", - "syslog-0": "syslog.example.com:514", -} - -func parametersConfig(params map[string]string) string { - nonStringParams := []string{ - "enable_monitoring", - "max_disk_threshold", - "metrics_frequency", - "java_heapspace", - "java_maxmetaspace", - "plugins", - "syslog", - "tls_ciphers", - } - parameters := "parameters = {" - for k, v := range params { - if utils.Contains(nonStringParams, k) { - parameters += fmt.Sprintf("%s = %s\n", k, v) - } else { - parameters += fmt.Sprintf("%s = %q\n", k, v) - } - } - parameters += "\n}" - return parameters -} - -func resourceConfig(params map[string]string) string { - return fmt.Sprintf(` - %s - - resource "stackit_opensearch_instance" "instance" { - project_id = "%s" - name = "%s" - plan_name = "%s" - version = "%s" - %s - } - - resource "stackit_opensearch_credential" "credential" { - project_id = stackit_opensearch_instance.instance.project_id - instance_id = stackit_opensearch_instance.instance.instance_id - } - `, - testutil.OpenSearchProviderConfig(), - instanceResource["project_id"], - instanceResource["name"], - instanceResource["plan_name"], - instanceResource["version"], - parametersConfig(params), - ) -} - -func TestAccOpenSearchResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckOpenSearchDestroy, - Steps: []resource.TestStep{ - - // Creation - { - Config: resourceConfig( - map[string]string{ - "sgw_acl": instanceResource["sgw_acl-1"], - "max_disk_threshold": instanceResource["max_disk_threshold"], - "enable_monitoring": instanceResource["enable_monitoring"], - "syslog": fmt.Sprintf(`[%q]`, instanceResource["syslog-0"]), - }), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_opensearch_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "plan_name", instanceResource["plan_name"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl-1"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.max_disk_threshold", instanceResource["max_disk_threshold"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.enable_monitoring", instanceResource["enable_monitoring"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.syslog.#", "1"), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.syslog.0", instanceResource["syslog-0"]), - - // Credential data - resource.TestCheckResourceAttrPair( - "stackit_opensearch_credential.credential", "project_id", - "stackit_opensearch_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_opensearch_credential.credential", "instance_id", - "stackit_opensearch_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_opensearch_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_opensearch_credential.credential", "host"), - ), - }, - // Data source - { - Config: fmt.Sprintf(` - %s - - data "stackit_opensearch_instance" "instance" { - project_id = stackit_opensearch_instance.instance.project_id - instance_id = stackit_opensearch_instance.instance.instance_id - } - - data "stackit_opensearch_credential" "credential" { - project_id = stackit_opensearch_credential.credential.project_id - instance_id = stackit_opensearch_credential.credential.instance_id - credential_id = stackit_opensearch_credential.credential.credential_id - }`, - resourceConfig( - map[string]string{ - "sgw_acl": instanceResource["sgw_acl-1"], - "max_disk_threshold": instanceResource["max_disk_threshold"], - "enable_monitoring": instanceResource["enable_monitoring"], - "syslog": fmt.Sprintf(`[%q]`, instanceResource["syslog-0"]), - }), - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrPair("stackit_opensearch_instance.instance", "instance_id", - "data.stackit_opensearch_instance.instance", "instance_id"), - resource.TestCheckResourceAttrPair("stackit_opensearch_credential.credential", "credential_id", - "data.stackit_opensearch_credential.credential", "credential_id"), - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl-1"]), - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "parameters.max_disk_threshold", instanceResource["max_disk_threshold"]), - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "parameters.enable_monitoring", instanceResource["enable_monitoring"]), - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "parameters.syslog.#", "1"), - resource.TestCheckResourceAttr("data.stackit_opensearch_instance.instance", "parameters.syslog.0", instanceResource["syslog-0"]), - - // Credential data - resource.TestCheckResourceAttr("data.stackit_opensearch_credential.credential", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("data.stackit_opensearch_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("data.stackit_opensearch_credential.credential", "host"), - resource.TestCheckResourceAttrSet("data.stackit_opensearch_credential.credential", "port"), - resource.TestCheckResourceAttrSet("data.stackit_opensearch_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("data.stackit_opensearch_credential.credential", "scheme"), - ), - }, - // Import - { - ResourceName: "stackit_opensearch_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_opensearch_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_opensearch_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "stackit_opensearch_credential.credential", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_opensearch_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_opensearch_credential.credential") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: resourceConfig( - map[string]string{ - "sgw_acl": instanceResource["sgw_acl-2"], - "max_disk_threshold": instanceResource["max_disk_threshold"], - "enable_monitoring": instanceResource["enable_monitoring"], - "syslog": fmt.Sprintf(`[%q]`, instanceResource["syslog-0"]), - }), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_opensearch_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "plan_name", instanceResource["plan_name"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl-2"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.max_disk_threshold", instanceResource["max_disk_threshold"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.enable_monitoring", instanceResource["enable_monitoring"]), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.syslog.#", "1"), - resource.TestCheckResourceAttr("stackit_opensearch_instance.instance", "parameters.syslog.0", instanceResource["syslog-0"]), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckOpenSearchDestroy(s *terraform.State) error { - ctx := context.Background() - var client *opensearch.APIClient - var err error - if testutil.OpenSearchCustomEndpoint == "" { - client, err = opensearch.NewAPIClient( - config.WithRegion("eu01"), - ) - } else { - client, err = opensearch.NewAPIClient( - config.WithEndpoint(testutil.OpenSearchCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_opensearch_instance" { - continue - } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := *instancesResp.Instances - for i := range instances { - if instances[i].InstanceId == nil { - continue - } - if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { - if !checkInstanceDeleteSuccess(&instances[i]) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) - } - } - } - } - return nil -} - -func checkInstanceDeleteSuccess(i *opensearch.Instance) bool { - if *i.LastOperation.Type != opensearch.INSTANCELASTOPERATIONTYPE_DELETE { - return false - } - - if *i.LastOperation.Type == opensearch.INSTANCELASTOPERATIONTYPE_DELETE { - if *i.LastOperation.State != opensearch.INSTANCELASTOPERATIONSTATE_SUCCEEDED { - return false - } else if strings.Contains(*i.LastOperation.Description, "DeleteFailed") || strings.Contains(*i.LastOperation.Description, "failed") { - return false - } - } - return true -} diff --git a/stackit/internal/services/opensearch/utils/util.go b/stackit/internal/services/opensearch/utils/util.go deleted file mode 100644 index e630a860..00000000 --- a/stackit/internal/services/opensearch/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *opensearch.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.OpenSearchCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.OpenSearchCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := opensearch.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/opensearch/utils/util_test.go b/stackit/internal/services/opensearch/utils/util_test.go deleted file mode 100644 index fa46355b..00000000 --- a/stackit/internal/services/opensearch/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://opensearch-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *opensearch.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *opensearch.APIClient { - apiClient, err := opensearch.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - OpenSearchCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *opensearch.APIClient { - apiClient, err := opensearch.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/postgresflex/database/datasource.go b/stackit/internal/services/postgresflex/database/datasource.go deleted file mode 100644 index 7eec59fb..00000000 --- a/stackit/internal/services/postgresflex/database/datasource.go +++ /dev/null @@ -1,171 +0,0 @@ -package postgresflex - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &databaseDataSource{} -) - -// NewDatabaseDataSource is a helper function to simplify the provider implementation. -func NewDatabaseDataSource() datasource.DataSource { - return &databaseDataSource{} -} - -// databaseDataSource is the data source implementation. -type databaseDataSource struct { - client *postgresflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *databaseDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflex_database" -} - -// Configure adds the provider configured client to the data source. -func (r *databaseDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex database client configured") -} - -// Schema defines the schema for the data source. -func (r *databaseDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".", - "database_id": "Database ID.", - "instance_id": "ID of the Postgres Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Database name.", - "owner": "Username of the database owner.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "database_id": schema.StringAttribute{ - Description: descriptions["database_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "owner": schema.StringAttribute{ - Description: descriptions["owner"], - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId := model.DatabaseId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) - ctx = tflog.SetField(ctx, "region", region) - - databaseResp, err := getDatabase(ctx, r.client, projectId, region, instanceId, databaseId) - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading database", - fmt.Sprintf("Database with ID %q or instance with ID %q does not exist in project %q.", databaseId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema and populate Computed attribute values - err = mapFields(databaseResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database read") -} diff --git a/stackit/internal/services/postgresflex/database/resource.go b/stackit/internal/services/postgresflex/database/resource.go deleted file mode 100644 index 809d7d23..00000000 --- a/stackit/internal/services/postgresflex/database/resource.go +++ /dev/null @@ -1,437 +0,0 @@ -package postgresflex - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &databaseResource{} - _ resource.ResourceWithConfigure = &databaseResource{} - _ resource.ResourceWithImportState = &databaseResource{} - _ resource.ResourceWithModifyPlan = &databaseResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - DatabaseId types.String `tfsdk:"database_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - Owner types.String `tfsdk:"owner"` - Region types.String `tfsdk:"region"` -} - -// NewDatabaseResource is a helper function to simplify the provider implementation. -func NewDatabaseResource() resource.Resource { - return &databaseResource{} -} - -// databaseResource is the resource implementation. -type databaseResource struct { - client *postgresflex.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *databaseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *databaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflex_database" -} - -// Configure adds the provider configured client to the resource. -func (r *databaseResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex database client configured") -} - -// Schema defines the schema for the resource. -func (r *databaseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex database resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`database_id`\".", - "database_id": "Database ID.", - "instance_id": "ID of the Postgres Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Database name.", - "owner": "Username of the database owner.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "database_id": schema.StringAttribute{ - Description: descriptions["database_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "owner": schema.StringAttribute{ - Description: descriptions["owner"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *databaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new database - databaseResp, err := r.client.CreateDatabase(ctx, projectId, region, instanceId).CreateDatabasePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if databaseResp == nil || databaseResp.Id == nil || *databaseResp.Id == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", "API didn't return database Id. A database might have been created") - return - } - databaseId := *databaseResp.Id - ctx = tflog.SetField(ctx, "database_id", databaseId) - - database, err := getDatabase(ctx, r.client, projectId, region, instanceId, databaseId) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Getting database details after creation: %v", err)) - return - } - - // Map response body to schema - err = mapFields(database, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId := model.DatabaseId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) - ctx = tflog.SetField(ctx, "region", region) - - databaseResp, err := getDatabase(ctx, r.client, projectId, region, instanceId, databaseId) - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, databaseNotFoundErr) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(databaseResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex database read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *databaseResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating database", "Database can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - databaseId := model.DatabaseId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "database_id", databaseId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing record set - err := r.client.DeleteDatabase(ctx, projectId, region, instanceId, databaseId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Postgres Flex database deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *databaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing database", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("database_id"), idParts[3])...) - core.LogAndAddWarning(ctx, &resp.Diagnostics, - "Postgresflex database imported with empty password", - "The database password is not imported as it is only available upon creation of a new database. The password field will be empty.", - ) - tflog.Info(ctx, "Postgres Flex database state imported") -} - -func mapFields(databaseResp *postgresflex.InstanceDatabase, model *Model, region string) error { - if databaseResp == nil { - return fmt.Errorf("response is nil") - } - if databaseResp.Id == nil || *databaseResp.Id == "" { - return fmt.Errorf("id not present") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var databaseId string - if model.DatabaseId.ValueString() != "" { - databaseId = model.DatabaseId.ValueString() - } else if databaseResp.Id != nil { - databaseId = *databaseResp.Id - } else { - return fmt.Errorf("database id not present") - } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), databaseId, - ) - model.DatabaseId = types.StringValue(databaseId) - model.Name = types.StringPointerValue(databaseResp.Name) - model.Region = types.StringValue(region) - - if databaseResp.Options != nil { - owner, ok := (*databaseResp.Options)["owner"] - if ok { - ownerStr, ok := owner.(string) - if !ok { - return fmt.Errorf("owner is not a string") - } - // If the field is returned between with quotes, we trim them to prevent an inconsistent result after apply - ownerStr = strings.TrimPrefix(ownerStr, `"`) - ownerStr = strings.TrimSuffix(ownerStr, `"`) - model.Owner = types.StringValue(ownerStr) - } - } - - return nil -} - -func toCreatePayload(model *Model) (*postgresflex.CreateDatabasePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &postgresflex.CreateDatabasePayload{ - Name: model.Name.ValueStringPointer(), - Options: &map[string]string{ - "owner": model.Owner.ValueString(), - }, - }, nil -} - -var databaseNotFoundErr = errors.New("database not found") - -// The API does not have a GetDatabase endpoint, only ListDatabases -func getDatabase(ctx context.Context, client *postgresflex.APIClient, projectId, region, instanceId, databaseId string) (*postgresflex.InstanceDatabase, error) { - resp, err := client.ListDatabases(ctx, projectId, region, instanceId).Execute() - if err != nil { - return nil, err - } - if resp == nil || resp.Databases == nil { - return nil, fmt.Errorf("response is nil") - } - for _, database := range *resp.Databases { - if database.Id != nil && *database.Id == databaseId { - return &database, nil - } - } - return nil, databaseNotFoundErr -} diff --git a/stackit/internal/services/postgresflex/database/resource_test.go b/stackit/internal/services/postgresflex/database/resource_test.go deleted file mode 100644 index 1770801b..00000000 --- a/stackit/internal/services/postgresflex/database/resource_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package postgresflex - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" -) - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflex.InstanceDatabase - region string - expected Model - isValid bool - }{ - { - "default_values", - &postgresflex.InstanceDatabase{ - Id: utils.Ptr("uid"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - DatabaseId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - Owner: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &postgresflex.InstanceDatabase{ - Id: utils.Ptr("uid"), - Name: utils.Ptr("dbname"), - Options: &map[string]interface{}{ - "owner": "username", - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - DatabaseId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("dbname"), - Owner: types.StringValue("username"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflex.InstanceDatabase{ - Id: utils.Ptr("uid"), - Name: utils.Ptr(""), - Options: &map[string]interface{}{ - "owner": "", - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - DatabaseId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue(""), - Owner: types.StringValue(""), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "empty_response", - &postgresflex.InstanceDatabase{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &postgresflex.InstanceDatabase{ - Id: utils.Ptr(""), - Name: utils.Ptr("dbname"), - Options: &map[string]interface{}{ - "owner": "username", - }, - }, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *postgresflex.CreateDatabasePayload - isValid bool - }{ - { - "default_values", - &Model{ - Name: types.StringValue("dbname"), - Owner: types.StringValue("username"), - }, - &postgresflex.CreateDatabasePayload{ - Name: utils.Ptr("dbname"), - Options: &map[string]string{ - "owner": "username", - }, - }, - true, - }, - { - "null_fields", - &Model{ - Name: types.StringNull(), - Owner: types.StringNull(), - }, - &postgresflex.CreateDatabasePayload{ - Name: nil, - Options: &map[string]string{ - "owner": "", - }, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/postgresflex/instance/datasource.go b/stackit/internal/services/postgresflex/instance/datasource.go deleted file mode 100644 index 8f5cea1e..00000000 --- a/stackit/internal/services/postgresflex/instance/datasource.go +++ /dev/null @@ -1,222 +0,0 @@ -package postgresflex - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *postgresflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflex_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`\".", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acl": "The Access Control List (ACL) for the PostgresFlex instance.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Computed: true, - }, - "backup_schedule": schema.StringAttribute{ - Computed: true, - }, - "flavor": schema.SingleNestedAttribute{ - Computed: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - }, - "description": schema.StringAttribute{ - Computed: true, - }, - "cpu": schema.Int64Attribute{ - Computed: true, - }, - "ram": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "replicas": schema.Int64Attribute{ - Computed: true, - }, - "storage": schema.SingleNestedAttribute{ - Computed: true, - Attributes: map[string]schema.Attribute{ - "class": schema.StringAttribute{ - Computed: true, - }, - "size": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "version": schema.StringAttribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - instanceResp, err := r.client.GetInstance(ctx, projectId, region, instanceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - if instanceResp != nil && instanceResp.Item != nil && instanceResp.Item.Status != nil && *instanceResp.Item.Status == wait.InstanceStateDeleted { - resp.State.RemoveResource(ctx) - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", "Instance was deleted successfully") - return - } - - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err = mapFields(ctx, instanceResp, &model, flavor, storage, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex instance read") -} diff --git a/stackit/internal/services/postgresflex/instance/resource.go b/stackit/internal/services/postgresflex/instance/resource.go deleted file mode 100644 index a4526e08..00000000 --- a/stackit/internal/services/postgresflex/instance/resource.go +++ /dev/null @@ -1,757 +0,0 @@ -package postgresflex - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - "time" - - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} - _ resource.ResourceWithModifyPlan = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - ACL types.List `tfsdk:"acl"` - BackupSchedule types.String `tfsdk:"backup_schedule"` - Flavor types.Object `tfsdk:"flavor"` - Replicas types.Int64 `tfsdk:"replicas"` - Storage types.Object `tfsdk:"storage"` - Version types.String `tfsdk:"version"` - Region types.String `tfsdk:"region"` -} - -// Struct corresponding to Model.Flavor -type flavorModel struct { - Id types.String `tfsdk:"id"` - Description types.String `tfsdk:"description"` - CPU types.Int64 `tfsdk:"cpu"` - RAM types.Int64 `tfsdk:"ram"` -} - -// Types corresponding to flavorModel -var flavorTypes = map[string]attr.Type{ - "id": basetypes.StringType{}, - "description": basetypes.StringType{}, - "cpu": basetypes.Int64Type{}, - "ram": basetypes.Int64Type{}, -} - -// Struct corresponding to Model.Storage -type storageModel struct { - Class types.String `tfsdk:"class"` - Size types.Int64 `tfsdk:"size"` -} - -// Types corresponding to storageModel -var storageTypes = map[string]attr.Type{ - "class": basetypes.StringType{}, - "size": basetypes.Int64Type{}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *postgresflex.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflex_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\".", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acl": "The Access Control List (ACL) for the PostgresFlex instance.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-z]([-a-z0-9]*[a-z0-9])?$"), - "must start with a letter, must have lower case letters, numbers or hyphens, and no hyphen at the end", - ), - }, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Required: true, - }, - "backup_schedule": schema.StringAttribute{ - Required: true, - }, - "flavor": schema.SingleNestedAttribute{ - Required: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - UseStateForUnknownIfFlavorUnchanged(req), - }, - }, - "description": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - UseStateForUnknownIfFlavorUnchanged(req), - }, - }, - "cpu": schema.Int64Attribute{ - Required: true, - }, - "ram": schema.Int64Attribute{ - Required: true, - }, - }, - }, - "replicas": schema.Int64Attribute{ - Required: true, - }, - "storage": schema.SingleNestedAttribute{ - Required: true, - Attributes: map[string]schema.Attribute{ - "class": schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "size": schema.Int64Attribute{ - Required: true, - }, - }, - }, - "version": schema.StringAttribute{ - Required: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - err := loadFlavorId(ctx, r.client, &model, flavor) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err)) - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, acl, flavor, storage) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId, region).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.Id - ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, flavor, storage, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - instanceResp, err := r.client.GetInstance(ctx, projectId, region, instanceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - if instanceResp != nil && instanceResp.Item != nil && instanceResp.Item.Status != nil && *instanceResp.Item.Status == wait.InstanceStateDeleted { - resp.State.RemoveResource(ctx) - return - } - - // Map response body to schema - err = mapFields(ctx, instanceResp, &model, flavor, storage, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - err := loadFlavorId(ctx, r.client, &model, flavor) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err)) - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, acl, flavor, storage) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - _, err = r.client.PartialUpdateInstance(ctx, projectId, region, instanceId).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, flavor, storage, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgresflex instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, region, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).SetTimeout(45 * time.Minute).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "Postgres Flex instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - tflog.Info(ctx, "Postgres Flex instance state imported") -} - -func mapFields(ctx context.Context, resp *postgresflex.InstanceResponse, model *Model, flavor *flavorModel, storage *storageModel, region string) error { - if resp == nil { - return fmt.Errorf("response input is nil") - } - if resp.Item == nil { - return fmt.Errorf("no instance provided") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - instance := resp.Item - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.Id != nil { - instanceId = *instance.Id - } else { - return fmt.Errorf("instance id not present") - } - - var aclList basetypes.ListValue - var diags diag.Diagnostics - if instance.Acl == nil || instance.Acl.Items == nil { - aclList = types.ListNull(types.StringType) - } else { - respACL := *instance.Acl.Items - modelACL, err := utils.ListValuetoStringSlice(model.ACL) - if err != nil { - return err - } - - reconciledACL := utils.ReconcileStringSlices(modelACL, respACL) - - aclList, diags = types.ListValueFrom(ctx, types.StringType, reconciledACL) - if diags.HasError() { - return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) - } - } - - var flavorValues map[string]attr.Value - if instance.Flavor == nil { - flavorValues = map[string]attr.Value{ - "id": flavor.Id, - "description": flavor.Description, - "cpu": flavor.CPU, - "ram": flavor.RAM, - } - } else { - flavorValues = map[string]attr.Value{ - "id": types.StringValue(*instance.Flavor.Id), - "description": types.StringValue(*instance.Flavor.Description), - "cpu": types.Int64PointerValue(instance.Flavor.Cpu), - "ram": types.Int64PointerValue(instance.Flavor.Memory), - } - } - flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues) - if diags.HasError() { - return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags)) - } - - var storageValues map[string]attr.Value - if instance.Storage == nil { - storageValues = map[string]attr.Value{ - "class": storage.Class, - "size": storage.Size, - } - } else { - storageValues = map[string]attr.Value{ - "class": types.StringValue(*instance.Storage.Class), - "size": types.Int64PointerValue(instance.Storage.Size), - } - } - storageObject, diags := types.ObjectValue(storageTypes, storageValues) - if diags.HasError() { - return fmt.Errorf("creating storage: %w", core.DiagsToError(diags)) - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, instanceId) - model.InstanceId = types.StringValue(instanceId) - model.Name = types.StringPointerValue(instance.Name) - model.ACL = aclList - model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule) - model.Flavor = flavorObject - model.Replicas = types.Int64PointerValue(instance.Replicas) - model.Storage = storageObject - model.Version = types.StringPointerValue(instance.Version) - model.Region = types.StringValue(region) - return nil -} - -func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *storageModel) (*postgresflex.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if acl == nil { - return nil, fmt.Errorf("nil acl") - } - if flavor == nil { - return nil, fmt.Errorf("nil flavor") - } - if storage == nil { - return nil, fmt.Errorf("nil storage") - } - - return &postgresflex.CreateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &acl, - }, - BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule), - FlavorId: conversion.StringValueToPointer(flavor.Id), - Name: conversion.StringValueToPointer(model.Name), - Replicas: conversion.Int64ValueToPointer(model.Replicas), - Storage: &postgresflex.Storage{ - Class: conversion.StringValueToPointer(storage.Class), - Size: conversion.Int64ValueToPointer(storage.Size), - }, - Version: conversion.StringValueToPointer(model.Version), - }, nil -} - -func toUpdatePayload(model *Model, acl []string, flavor *flavorModel, storage *storageModel) (*postgresflex.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if acl == nil { - return nil, fmt.Errorf("nil acl") - } - if flavor == nil { - return nil, fmt.Errorf("nil flavor") - } - if storage == nil { - return nil, fmt.Errorf("nil storage") - } - - return &postgresflex.PartialUpdateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &acl, - }, - BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule), - FlavorId: conversion.StringValueToPointer(flavor.Id), - Name: conversion.StringValueToPointer(model.Name), - Replicas: conversion.Int64ValueToPointer(model.Replicas), - Storage: &postgresflex.Storage{ - Class: conversion.StringValueToPointer(storage.Class), - Size: conversion.Int64ValueToPointer(storage.Size), - }, - Version: conversion.StringValueToPointer(model.Version), - }, nil -} - -type postgresFlexClient interface { - ListFlavorsExecute(ctx context.Context, projectId string, region string) (*postgresflex.ListFlavorsResponse, error) -} - -func loadFlavorId(ctx context.Context, client postgresFlexClient, model *Model, flavor *flavorModel) error { - if model == nil { - return fmt.Errorf("nil model") - } - if flavor == nil { - return fmt.Errorf("nil flavor") - } - cpu := conversion.Int64ValueToPointer(flavor.CPU) - if cpu == nil { - return fmt.Errorf("nil CPU") - } - ram := conversion.Int64ValueToPointer(flavor.RAM) - if ram == nil { - return fmt.Errorf("nil RAM") - } - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - res, err := client.ListFlavorsExecute(ctx, projectId, region) - if err != nil { - return fmt.Errorf("listing postgresflex flavors: %w", err) - } - - avl := "" - if res.Flavors == nil { - return fmt.Errorf("finding flavors for project %s", projectId) - } - for _, f := range *res.Flavors { - if f.Id == nil || f.Cpu == nil || f.Memory == nil { - continue - } - if *f.Cpu == *cpu && *f.Memory == *ram { - flavor.Id = types.StringValue(*f.Id) - flavor.Description = types.StringValue(*f.Description) - break - } - avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory) - } - if flavor.Id.ValueString() == "" { - return fmt.Errorf("couldn't find flavor, available specs are:%s", avl) - } - - return nil -} diff --git a/stackit/internal/services/postgresflex/instance/resource_test.go b/stackit/internal/services/postgresflex/instance/resource_test.go deleted file mode 100644 index 4b3c5807..00000000 --- a/stackit/internal/services/postgresflex/instance/resource_test.go +++ /dev/null @@ -1,770 +0,0 @@ -package postgresflex - -import ( - "context" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" -) - -type postgresFlexClientMocked struct { - returnError bool - getFlavorsResp *postgresflex.ListFlavorsResponse -} - -func (c *postgresFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*postgresflex.ListFlavorsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("get flavors failed") - } - - return c.getFlavorsResp, nil -} - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - state Model - input *postgresflex.InstanceResponse - flavor *flavorModel - storage *storageModel - region string - expected Model - isValid bool - }{ - { - "default_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{}, - }, - &flavorModel{}, - &storageModel{}, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - ACL: types.ListNull(types.StringType), - BackupSchedule: types.StringNull(), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Null(), - "ram": types.Int64Null(), - }), - Replicas: types.Int64Null(), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringNull(), - "size": types.Int64Null(), - }), - Version: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Acl: &postgresflex.ACL{ - Items: &[]string{ - "ip1", - "ip2", - "", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: &postgresflex.Flavor{ - Cpu: utils.Ptr(int64(12)), - Description: utils.Ptr("description"), - Id: utils.Ptr("flavor_id"), - Memory: utils.Ptr(int64(34)), - }, - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: utils.Ptr("status"), - Storage: &postgresflex.Storage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(78)), - }, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{}, - &storageModel{}, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip1"), - types.StringValue("ip2"), - types.StringValue(""), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringValue("flavor_id"), - "description": types.StringValue("description"), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Version: types.StringValue("version"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values_no_flavor_and_storage", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Acl: &postgresflex.ACL{ - Items: &[]string{ - "ip1", - "ip2", - "", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: nil, - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: utils.Ptr("status"), - Storage: nil, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{ - CPU: types.Int64Value(12), - RAM: types.Int64Value(34), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(78), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip1"), - types.StringValue("ip2"), - types.StringValue(""), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Version: types.StringValue("version"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "acl_unordered", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip2"), - types.StringValue(""), - types.StringValue("ip1"), - }), - }, - &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Acl: &postgresflex.ACL{ - Items: &[]string{ - "", - "ip1", - "ip2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: nil, - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: utils.Ptr("status"), - Storage: nil, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{ - CPU: types.Int64Value(12), - RAM: types.Int64Value(34), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(78), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip2"), - types.StringValue(""), - types.StringValue("ip1"), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Version: types.StringValue("version"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - nil, - &flavorModel{}, - &storageModel{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &postgresflex.InstanceResponse{}, - &flavorModel{}, - &storageModel{}, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state, tt.flavor, tt.storage, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputAcl []string - inputFlavor *flavorModel - inputStorage *storageModel - expected *postgresflex.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - &postgresflex.CreateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &[]string{}, - }, - Storage: &postgresflex.Storage{}, - }, - true, - }, - { - "simple_values", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Name: types.StringValue("name"), - Replicas: types.Int64Value(12), - Version: types.StringValue("version"), - }, - []string{ - "ip_1", - "ip_2", - }, - &flavorModel{ - Id: types.StringValue("flavor_id"), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(34), - }, - &postgresflex.CreateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &[]string{ - "ip_1", - "ip_2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - FlavorId: utils.Ptr("flavor_id"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(12)), - Storage: &postgresflex.Storage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(34)), - }, - Version: utils.Ptr("version"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - BackupSchedule: types.StringNull(), - Name: types.StringNull(), - Replicas: types.Int64Value(2123456789), - Version: types.StringNull(), - }, - []string{ - "", - }, - &flavorModel{ - Id: types.StringNull(), - }, - &storageModel{ - Class: types.StringNull(), - Size: types.Int64Null(), - }, - &postgresflex.CreateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &[]string{ - "", - }, - }, - BackupSchedule: nil, - FlavorId: nil, - Name: nil, - Replicas: utils.Ptr(int64(2123456789)), - Storage: &postgresflex.Storage{ - Class: nil, - Size: nil, - }, - Version: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - &flavorModel{}, - &storageModel{}, - nil, - false, - }, - { - "nil_acl", - &Model{}, - nil, - &flavorModel{}, - &storageModel{}, - nil, - false, - }, - { - "nil_flavor", - &Model{}, - []string{}, - nil, - &storageModel{}, - nil, - false, - }, - { - "nil_storage", - &Model{}, - []string{}, - &flavorModel{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputAcl, tt.inputFlavor, tt.inputStorage) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputAcl []string - inputFlavor *flavorModel - inputStorage *storageModel - expected *postgresflex.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - &postgresflex.PartialUpdateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &[]string{}, - }, - Storage: &postgresflex.Storage{}, - }, - true, - }, - { - "simple_values", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Name: types.StringValue("name"), - Replicas: types.Int64Value(12), - Version: types.StringValue("version"), - }, - []string{ - "ip_1", - "ip_2", - }, - &flavorModel{ - Id: types.StringValue("flavor_id"), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(34), - }, - &postgresflex.PartialUpdateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &[]string{ - "ip_1", - "ip_2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - FlavorId: utils.Ptr("flavor_id"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(12)), - Storage: &postgresflex.Storage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(34)), - }, - Version: utils.Ptr("version"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - BackupSchedule: types.StringNull(), - Name: types.StringNull(), - Replicas: types.Int64Value(2123456789), - Version: types.StringNull(), - }, - []string{ - "", - }, - &flavorModel{ - Id: types.StringNull(), - }, - &storageModel{ - Class: types.StringNull(), - Size: types.Int64Null(), - }, - &postgresflex.PartialUpdateInstancePayload{ - Acl: &postgresflex.ACL{ - Items: &[]string{ - "", - }, - }, - BackupSchedule: nil, - FlavorId: nil, - Name: nil, - Replicas: utils.Ptr(int64(2123456789)), - Storage: &postgresflex.Storage{ - Class: nil, - Size: nil, - }, - Version: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - &flavorModel{}, - &storageModel{}, - nil, - false, - }, - { - "nil_acl", - &Model{}, - nil, - &flavorModel{}, - &storageModel{}, - nil, - false, - }, - { - "nil_flavor", - &Model{}, - []string{}, - nil, - &storageModel{}, - nil, - false, - }, - { - "nil_storage", - &Model{}, - []string{}, - &flavorModel{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputAcl, tt.inputFlavor, tt.inputStorage) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestLoadFlavorId(t *testing.T) { - tests := []struct { - description string - inputFlavor *flavorModel - mockedResp *postgresflex.ListFlavorsResponse - expected *flavorModel - getFlavorsFails bool - isValid bool - }{ - { - "ok_flavor", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(2)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - }, - }, - &flavorModel{ - Id: types.StringValue("fid-1"), - Description: types.StringValue("description"), - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - true, - }, - { - "ok_flavor_2", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(2)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - { - Id: utils.Ptr("fid-2"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - &flavorModel{ - Id: types.StringValue("fid-1"), - Description: types.StringValue("description"), - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - true, - }, - { - "no_matching_flavor", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - { - Id: utils.Ptr("fid-2"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - false, - }, - { - "nil_response", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &postgresflex.ListFlavorsResponse{}, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - false, - }, - { - "error_response", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &postgresflex.ListFlavorsResponse{}, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - true, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - returnError: tt.getFlavorsFails, - getFlavorsResp: tt.mockedResp, - } - model := &Model{ - ProjectId: types.StringValue("pid"), - } - flavorModel := &flavorModel{ - CPU: tt.inputFlavor.CPU, - RAM: tt.inputFlavor.RAM, - } - err := loadFlavorId(context.Background(), client, model, flavorModel) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(flavorModel, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go b/stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go deleted file mode 100644 index 38c924ba..00000000 --- a/stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go +++ /dev/null @@ -1,85 +0,0 @@ -package postgresflex - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" -) - -type useStateForUnknownIfFlavorUnchangedModifier struct { - Req resource.SchemaRequest -} - -// UseStateForUnknownIfFlavorUnchanged returns a plan modifier similar to UseStateForUnknown -// if the RAM and CPU values are not changed in the plan. Otherwise, the plan modifier does nothing. -func UseStateForUnknownIfFlavorUnchanged(req resource.SchemaRequest) planmodifier.String { - return useStateForUnknownIfFlavorUnchangedModifier{ - Req: req, - } -} - -func (m useStateForUnknownIfFlavorUnchangedModifier) Description(context.Context) string { - return "UseStateForUnknownIfFlavorUnchanged returns a plan modifier similar to UseStateForUnknown if the RAM and CPU values are not changed in the plan. Otherwise, the plan modifier does nothing." -} - -func (m useStateForUnknownIfFlavorUnchangedModifier) MarkdownDescription(ctx context.Context) string { - return m.Description(ctx) -} - -func (m useStateForUnknownIfFlavorUnchangedModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { // nolint:gocritic // function signature required by Terraform - // Do nothing if there is no state value. - if req.StateValue.IsNull() { - return - } - - // Do nothing if there is a known planned value. - if !req.PlanValue.IsUnknown() { - return - } - - // Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up. - if req.ConfigValue.IsUnknown() { - return - } - - // The above checks are taken from the UseStateForUnknown plan modifier implementation - // (https://github.com/hashicorp/terraform-plugin-framework/blob/main/resource/schema/stringplanmodifier/use_state_for_unknown.go#L38) - - var stateModel Model - diags := req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var stateFlavor = &flavorModel{} - if !(stateModel.Flavor.IsNull() || stateModel.Flavor.IsUnknown()) { - diags = stateModel.Flavor.As(ctx, stateFlavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var planModel Model - diags = req.Plan.Get(ctx, &planModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var planFlavor = &flavorModel{} - if !(planModel.Flavor.IsNull() || planModel.Flavor.IsUnknown()) { - diags = planModel.Flavor.As(ctx, planFlavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - if planFlavor.CPU == stateFlavor.CPU && planFlavor.RAM == stateFlavor.RAM { - resp.PlanValue = req.StateValue - } -} diff --git a/stackit/internal/services/postgresflex/postgresflex_acc_test.go b/stackit/internal/services/postgresflex/postgresflex_acc_test.go deleted file mode 100644 index 122633b3..00000000 --- a/stackit/internal/services/postgresflex/postgresflex_acc_test.go +++ /dev/null @@ -1,369 +0,0 @@ -package postgresflex_test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// Instance resource data -var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)), - "acl": "192.168.0.0/16", - "backup_schedule": "00 16 * * *", - "backup_schedule_updated": "00 12 * * *", - "flavor_cpu": "2", - "flavor_ram": "4", - "flavor_description": "Small, Compute optimized", - "replicas": "1", - "storage_class": "premium-perf12-stackit", - "storage_size": "5", - "version": "14", - "flavor_id": "2.4", -} - -// User resource data -var userResource = map[string]string{ - "username": fmt.Sprintf("tfaccuser%s", acctest.RandStringFromCharSet(4, acctest.CharSetAlpha)), - "role": "createdb", - "project_id": instanceResource["project_id"], -} - -// Database resource data -var databaseResource = map[string]string{ - "name": fmt.Sprintf("tfaccdb%s", acctest.RandStringFromCharSet(4, acctest.CharSetAlphaNum)), -} - -func configResources(backupSchedule string, region *string) string { - var regionConfig string - if region != nil { - regionConfig = fmt.Sprintf(`region = %q`, *region) - } - return fmt.Sprintf(` - %s - - resource "stackit_postgresflex_instance" "instance" { - project_id = "%s" - name = "%s" - acl = ["%s"] - backup_schedule = "%s" - flavor = { - cpu = %s - ram = %s - } - replicas = %s - storage = { - class = "%s" - size = %s - } - version = "%s" - %s - } - - resource "stackit_postgresflex_user" "user" { - project_id = stackit_postgresflex_instance.instance.project_id - instance_id = stackit_postgresflex_instance.instance.instance_id - username = "%s" - roles = ["%s"] - } - - resource "stackit_postgresflex_database" "database" { - project_id = stackit_postgresflex_instance.instance.project_id - instance_id = stackit_postgresflex_instance.instance.instance_id - name = "%s" - owner = stackit_postgresflex_user.user.username - } - `, - testutil.PostgresFlexProviderConfig(), - instanceResource["project_id"], - instanceResource["name"], - instanceResource["acl"], - backupSchedule, - instanceResource["flavor_cpu"], - instanceResource["flavor_ram"], - instanceResource["replicas"], - instanceResource["storage_class"], - instanceResource["storage_size"], - instanceResource["version"], - regionConfig, - userResource["username"], - userResource["role"], - databaseResource["name"], - ) -} - -func TestAccPostgresFlexFlexResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckPostgresFlexDestroy, - Steps: []resource.TestStep{ - // Creation - { - Config: configResources(instanceResource["backup_schedule"], &testutil.Region), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_postgresflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "acl.0", instanceResource["acl"]), - resource.TestCheckResourceAttrSet("stackit_postgresflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttrSet("stackit_postgresflex_instance.instance", "flavor.description"), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "backup_schedule", instanceResource["backup_schedule"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "flavor.cpu", instanceResource["flavor_cpu"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "flavor.ram", instanceResource["flavor_ram"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "replicas", instanceResource["replicas"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "storage.class", instanceResource["storage_class"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "storage.size", instanceResource["storage_size"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "region", testutil.Region), - - // User - resource.TestCheckResourceAttrPair( - "stackit_postgresflex_user.user", "project_id", - "stackit_postgresflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_postgresflex_user.user", "instance_id", - "stackit_postgresflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_postgresflex_user.user", "user_id"), - resource.TestCheckResourceAttrSet("stackit_postgresflex_user.user", "password"), - - // Database - resource.TestCheckResourceAttrPair( - "stackit_postgresflex_database.database", "project_id", - "stackit_postgresflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_postgresflex_database.database", "instance_id", - "stackit_postgresflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttr("stackit_postgresflex_database.database", "name", databaseResource["name"]), - resource.TestCheckResourceAttrPair( - "stackit_postgresflex_database.database", "owner", - "stackit_postgresflex_user.user", "username", - ), - ), - }, - // data source - { - Config: fmt.Sprintf(` - %s - - data "stackit_postgresflex_instance" "instance" { - project_id = stackit_postgresflex_instance.instance.project_id - instance_id = stackit_postgresflex_instance.instance.instance_id - } - - data "stackit_postgresflex_user" "user" { - project_id = stackit_postgresflex_instance.instance.project_id - instance_id = stackit_postgresflex_instance.instance.instance_id - user_id = stackit_postgresflex_user.user.user_id - } - - data "stackit_postgresflex_database" "database" { - project_id = stackit_postgresflex_instance.instance.project_id - instance_id = stackit_postgresflex_instance.instance.instance_id - database_id = stackit_postgresflex_database.database.database_id - } - `, - configResources(instanceResource["backup_schedule"], nil), - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttrPair( - "data.stackit_postgresflex_instance.instance", "project_id", - "stackit_postgresflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_postgresflex_instance.instance", "instance_id", - "stackit_postgresflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_postgresflex_user.user", "instance_id", - "stackit_postgresflex_user.user", "instance_id", - ), - - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "acl.0", instanceResource["acl"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "backup_schedule", instanceResource["backup_schedule"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "flavor.id", instanceResource["flavor_id"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "flavor.description", instanceResource["flavor_description"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "flavor.cpu", instanceResource["flavor_cpu"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "flavor.ram", instanceResource["flavor_ram"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_instance.instance", "replicas", instanceResource["replicas"]), - - // User data - resource.TestCheckResourceAttr("data.stackit_postgresflex_user.user", "project_id", userResource["project_id"]), - resource.TestCheckResourceAttrSet("data.stackit_postgresflex_user.user", "user_id"), - resource.TestCheckResourceAttr("data.stackit_postgresflex_user.user", "username", userResource["username"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_user.user", "roles.#", "1"), - resource.TestCheckResourceAttr("data.stackit_postgresflex_user.user", "roles.0", userResource["role"]), - resource.TestCheckResourceAttrSet("data.stackit_postgresflex_user.user", "host"), - resource.TestCheckResourceAttrSet("data.stackit_postgresflex_user.user", "port"), - - // Database data - resource.TestCheckResourceAttr("data.stackit_postgresflex_database.database", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttr("data.stackit_postgresflex_database.database", "name", databaseResource["name"]), - resource.TestCheckResourceAttrPair( - "data.stackit_postgresflex_database.database", "instance_id", - "stackit_postgresflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_postgresflex_database.database", "owner", - "data.stackit_postgresflex_user.user", "username", - ), - ), - }, - // Import - { - ResourceName: "stackit_postgresflex_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_postgresflex_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_postgresflex_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password"}, - }, - { - ResourceName: "stackit_postgresflex_user.user", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_postgresflex_user.user"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_postgresflex_user.user") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - userId, ok := r.Primary.Attributes["user_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute user_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId, userId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password", "uri"}, - }, - { - ResourceName: "stackit_postgresflex_database.database", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_postgresflex_database.database"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_postgresflex_database.database") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - databaseId, ok := r.Primary.Attributes["database_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute database_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId, databaseId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: configResources(instanceResource["backup_schedule_updated"], nil), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_postgresflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "acl.0", instanceResource["acl"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "backup_schedule", instanceResource["backup_schedule_updated"]), - resource.TestCheckResourceAttrSet("stackit_postgresflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttrSet("stackit_postgresflex_instance.instance", "flavor.description"), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "flavor.cpu", instanceResource["flavor_cpu"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "flavor.ram", instanceResource["flavor_ram"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "replicas", instanceResource["replicas"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "storage.class", instanceResource["storage_class"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "storage.size", instanceResource["storage_size"]), - resource.TestCheckResourceAttr("stackit_postgresflex_instance.instance", "version", instanceResource["version"]), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckPostgresFlexDestroy(s *terraform.State) error { - ctx := context.Background() - var client *postgresflex.APIClient - var err error - if testutil.PostgresFlexCustomEndpoint == "" { - client, err = postgresflex.NewAPIClient() - } else { - client, err = postgresflex.NewAPIClient( - config.WithEndpoint(testutil.PostgresFlexCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_postgresflex_instance" { - continue - } - // instance terraform ID: = "[project_id],[region],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[2] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - items := *instancesResp.Items - for i := range items { - if items[i].Id == nil { - continue - } - if utils.Contains(instancesToDestroy, *items[i].Id) { - err := client.ForceDeleteInstanceExecute(ctx, testutil.ProjectId, testutil.Region, *items[i].Id) - if err != nil { - return fmt.Errorf("deleting instance %s during CheckDestroy: %w", *items[i].Id, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, *items[i].Id).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("deleting instance %s during CheckDestroy: waiting for deletion %w", *items[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/postgresflex/user/datasource.go b/stackit/internal/services/postgresflex/user/datasource.go deleted file mode 100644 index ea43c85d..00000000 --- a/stackit/internal/services/postgresflex/user/datasource.go +++ /dev/null @@ -1,230 +0,0 @@ -package postgresflex - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &userDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` -} - -// NewUserDataSource is a helper function to simplify the provider implementation. -func NewUserDataSource() datasource.DataSource { - return &userDataSource{} -} - -// userDataSource is the data source implementation. -type userDataSource struct { - client *postgresflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflex_user" -} - -// Configure adds the provider configured client to the data source. -func (r *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex user client configured") -} - -// Schema defines the schema for the data source. -func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Postgres Flex user data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - "roles": schema.SetAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found automatically, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - ctx = tflog.SetField(ctx, "region", region) - - recordSetResp, err := r.client.GetUser(ctx, projectId, region, instanceId, userId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading user", - fmt.Sprintf("User with ID %q or instance with ID %q does not exist in project %q.", userId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema and populate Computed attribute values - err = mapDataSourceFields(recordSetResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex user read") -} - -func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSourceModel, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, - ) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - model.Region = types.StringValue(region) - return nil -} diff --git a/stackit/internal/services/postgresflex/user/datasource_test.go b/stackit/internal/services/postgresflex/user/datasource_test.go deleted file mode 100644 index ac824ccb..00000000 --- a/stackit/internal/services/postgresflex/user/datasource_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package postgresflex - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" -) - -func TestMapDataSourceFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflex.GetUserResponse - region string - expected DataSourceModel - isValid bool - }{ - { - "default_values", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{}, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{ - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{ - Id: utils.Ptr("uid"), - Roles: &[]string{}, - Username: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - DataSourceModel{}, - false, - }, - { - "nil_response_2", - &postgresflex.GetUserResponse{}, - testRegion, - DataSourceModel{}, - false, - }, - { - "no_resource_id", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{}, - }, - testRegion, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &DataSourceModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapDataSourceFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/postgresflex/user/resource.go b/stackit/internal/services/postgresflex/user/resource.go deleted file mode 100644 index 2b1416e3..00000000 --- a/stackit/internal/services/postgresflex/user/resource.go +++ /dev/null @@ -1,577 +0,0 @@ -package postgresflex - -import ( - "context" - "fmt" - "net/http" - "strings" - - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} - _ resource.ResourceWithImportState = &userResource{} - _ resource.ResourceWithModifyPlan = &userResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Password types.String `tfsdk:"password"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Uri types.String `tfsdk:"uri"` - Region types.String `tfsdk:"region"` -} - -// NewUserResource is a helper function to simplify the provider implementation. -func NewUserResource() resource.Resource { - return &userResource{} -} - -// userResource is the resource implementation. -type userResource struct { - client *postgresflex.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_postgresflex_user" -} - -// Configure adds the provider configured client to the resource. -func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Postgres Flex user client configured") -} - -// Schema defines the schema for the resource. -func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - rolesOptions := []string{"login", "createdb"} - - descriptions := map[string]string{ - "main": "Postgres Flex user resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the PostgresFlex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "roles": "Database access levels for the user. " + utils.FormatPossibleValues(rolesOptions...), - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Required: true, - Validators: []validator.Set{ - setvalidator.ValueStringsAre( - stringvalidator.OneOf("login", "createdb"), - ), - }, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { - diags = model.Roles.ElementsAs(ctx, &roles, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, roles) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new user - userResp, err := r.client.CreateUser(ctx, projectId, region, instanceId).CreateUserPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user Id. A user might have been created") - return - } - userId := *userResp.Item.Id - ctx = tflog.SetField(ctx, "user_id", userId) - - // Map response body to schema - err = mapFieldsCreate(userResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex user created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - ctx = tflog.SetField(ctx, "region", region) - - recordSetResp, err := r.client.GetUser(ctx, projectId, region, instanceId, userId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(recordSetResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex user read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - ctx = tflog.SetField(ctx, "region", region) - - // Retrieve values from state - var stateModel Model - diags = req.State.Get(ctx, &stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { - diags = model.Roles.ElementsAs(ctx, &roles, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, roles) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Updating API payload: %v", err)) - return - } - - // Update existing instance - err = r.client.UpdateUser(ctx, projectId, region, instanceId, userId).UpdateUserPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - userResp, err := r.client.GetUser(ctx, projectId, region, instanceId, userId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API: %v", err)) - return - } - - // Map response body to schema - err = mapFields(userResp, &stateModel, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, stateModel) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Postgres Flex user updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing record set - err := r.client.DeleteUser(ctx, projectId, region, instanceId, userId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteUserWaitHandler(ctx, r.client, projectId, region, instanceId, userId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "Postgres Flex user deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing user", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[3])...) - core.LogAndAddWarning(ctx, &resp.Diagnostics, - "Postgresflex user imported with empty password and empty uri", - "The user password and uri are not imported as they are only available upon creation of a new user. The password and uri fields will be empty.", - ) - tflog.Info(ctx, "Postgresflex user state imported") -} - -func mapFieldsCreate(userResp *postgresflex.CreateUserResponse, model *Model, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - if user.Id == nil { - return fmt.Errorf("user id not present") - } - userId := *user.Id - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, - ) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - - if user.Password == nil { - return fmt.Errorf("user password not present") - } - model.Password = types.StringValue(*user.Password) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - model.Uri = types.StringPointerValue(user.Uri) - model.Region = types.StringValue(region) - return nil -} - -func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, - ) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - model.Region = types.StringValue(region) - return nil -} - -func toCreatePayload(model *Model, roles []string) (*postgresflex.CreateUserPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &postgresflex.CreateUserPayload{ - Roles: &roles, - Username: conversion.StringValueToPointer(model.Username), - }, nil -} - -func toUpdatePayload(model *Model, roles []string) (*postgresflex.UpdateUserPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if roles == nil { - return nil, fmt.Errorf("nil roles") - } - - return &postgresflex.UpdateUserPayload{ - Roles: &roles, - }, nil -} diff --git a/stackit/internal/services/postgresflex/user/resource_test.go b/stackit/internal/services/postgresflex/user/resource_test.go deleted file mode 100644 index b5c13716..00000000 --- a/stackit/internal/services/postgresflex/user/resource_test.go +++ /dev/null @@ -1,470 +0,0 @@ -package postgresflex - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" -) - -func TestMapFieldsCreate(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflex.CreateUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &postgresflex.CreateUserResponse{ - Item: &postgresflex.User{ - Id: utils.Ptr("uid"), - Password: utils.Ptr(""), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Password: types.StringValue(""), - Host: types.StringNull(), - Port: types.Int64Null(), - Uri: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &postgresflex.CreateUserResponse{ - Item: &postgresflex.User{ - Id: utils.Ptr("uid"), - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Password: utils.Ptr("password"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Password: types.StringValue("password"), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflex.CreateUserResponse{ - Item: &postgresflex.User{ - Id: utils.Ptr("uid"), - Roles: &[]string{}, - Username: nil, - Password: utils.Ptr(""), - Host: nil, - Port: utils.Ptr(int64(2123456789)), - Uri: nil, - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Password: types.StringValue(""), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Uri: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &postgresflex.CreateUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &postgresflex.CreateUserResponse{ - Item: &postgresflex.User{}, - }, - testRegion, - Model{}, - false, - }, - { - "no_password", - &postgresflex.CreateUserResponse{ - Item: &postgresflex.User{ - Id: utils.Ptr("uid"), - }, - }, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFieldsCreate(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *postgresflex.GetUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{}, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{ - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{ - Id: utils.Ptr("uid"), - Roles: &[]string{}, - Username: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &postgresflex.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{}, - }, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles []string - expected *postgresflex.CreateUserPayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &postgresflex.CreateUserPayload{ - Roles: &[]string{}, - Username: nil, - }, - true, - }, - { - "default_values", - &Model{ - Username: types.StringValue("username"), - }, - []string{ - "role_1", - "role_2", - }, - &postgresflex.CreateUserPayload{ - Roles: &[]string{ - "role_1", - "role_2", - }, - Username: utils.Ptr("username"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - }, - []string{ - "", - }, - &postgresflex.CreateUserPayload{ - Roles: &[]string{ - "", - }, - Username: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputRoles) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles []string - expected *postgresflex.UpdateUserPayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &postgresflex.UpdateUserPayload{ - Roles: &[]string{}, - }, - true, - }, - { - "default_values", - &Model{ - Username: types.StringValue("username"), - }, - []string{ - "role_1", - "role_2", - }, - &postgresflex.UpdateUserPayload{ - Roles: &[]string{ - "role_1", - "role_2", - }, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - }, - []string{ - "", - }, - &postgresflex.UpdateUserPayload{ - Roles: &[]string{ - "", - }, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - nil, - false, - }, - { - "nil_roles", - &Model{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputRoles) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/postgresflex/utils/util.go b/stackit/internal/services/postgresflex/utils/util.go deleted file mode 100644 index 478bb142..00000000 --- a/stackit/internal/services/postgresflex/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *postgresflex.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.PostgresFlexCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.PostgresFlexCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := postgresflex.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/postgresflex/utils/util_test.go b/stackit/internal/services/postgresflex/utils/util_test.go deleted file mode 100644 index 4af08da6..00000000 --- a/stackit/internal/services/postgresflex/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://postgresflex-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *postgresflex.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *postgresflex.APIClient { - apiClient, err := postgresflex.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - PostgresFlexCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *postgresflex.APIClient { - apiClient, err := postgresflex.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/rabbitmq/credential/datasource.go b/stackit/internal/services/rabbitmq/credential/datasource.go deleted file mode 100644 index be84cf0f..00000000 --- a/stackit/internal/services/rabbitmq/credential/datasource.go +++ /dev/null @@ -1,188 +0,0 @@ -package rabbitmq - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &credentialDataSource{} -) - -// NewCredentialDataSource is a helper function to simplify the provider implementation. -func NewCredentialDataSource() datasource.DataSource { - return &credentialDataSource{} -} - -// credentialDataSource is the data source implementation. -type credentialDataSource struct { - client *rabbitmq.APIClient -} - -// Metadata returns the data source type name. -func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_rabbitmq_credential" -} - -// Configure adds the provider configured client to the data source. -func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "RabbitMQ credential client configured") -} - -// Schema defines the schema for the data source. -func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "RabbitMQ credential data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the RabbitMQ instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "http_api_uri": schema.StringAttribute{ - Computed: true, - }, - "http_api_uris": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "management": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "uris": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading credential", - fmt.Sprintf("Credential with ID %q or instance with ID %q does not exist in project %q.", credentialId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "RabbitMQ credential read") -} diff --git a/stackit/internal/services/rabbitmq/credential/resource.go b/stackit/internal/services/rabbitmq/credential/resource.go deleted file mode 100644 index 5fac9bec..00000000 --- a/stackit/internal/services/rabbitmq/credential/resource.go +++ /dev/null @@ -1,423 +0,0 @@ -package rabbitmq - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialResource{} - _ resource.ResourceWithConfigure = &credentialResource{} - _ resource.ResourceWithImportState = &credentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialId types.String `tfsdk:"credential_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Host types.String `tfsdk:"host"` - Hosts types.List `tfsdk:"hosts"` - HttpAPIURI types.String `tfsdk:"http_api_uri"` - HttpAPIURIs types.List `tfsdk:"http_api_uris"` - Management types.String `tfsdk:"management"` - Password types.String `tfsdk:"password"` - Port types.Int64 `tfsdk:"port"` - Uri types.String `tfsdk:"uri"` - Uris types.List `tfsdk:"uris"` - Username types.String `tfsdk:"username"` -} - -// NewCredentialResource is a helper function to simplify the provider implementation. -func NewCredentialResource() resource.Resource { - return &credentialResource{} -} - -// credentialResource is the resource implementation. -type credentialResource struct { - client *rabbitmq.APIClient -} - -// Metadata returns the resource type name. -func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_rabbitmq_credential" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "RabbitMQ credential client configured") -} - -// Schema defines the schema for the resource. -func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "RabbitMQ credential resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the RabbitMQ instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "http_api_uri": schema.StringAttribute{ - Computed: true, - }, - "http_api_uris": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "management": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "uris": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Create new recordset - credentialsResp, err := r.client.CreateCredentials(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if credentialsResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") - return - } - credentialId := *credentialsResp.Id - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "RabbitMQ credential created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "RabbitMQ credential read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - // Delete existing record set - err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "RabbitMQ credential deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id -func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...) - tflog.Info(ctx, "RabbitMQ credential state imported") -} - -func mapFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsResponse, model *Model) error { - if credentialsResp == nil { - return fmt.Errorf("response input is nil") - } - if credentialsResp.Raw == nil { - return fmt.Errorf("response credentials raw is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - credentials := credentialsResp.Raw.Credentials - - var credentialId string - if model.CredentialId.ValueString() != "" { - credentialId = model.CredentialId.ValueString() - } else if credentialsResp.Id != nil { - credentialId = *credentialsResp.Id - } else { - return fmt.Errorf("credentials id not present") - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId, - ) - model.CredentialId = types.StringValue(credentialId) - - modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) - if err != nil { - return err - } - modelHttpApiUris, err := utils.ListValuetoStringSlice(model.HttpAPIURIs) - if err != nil { - return err - } - modelUris, err := utils.ListValuetoStringSlice(model.Uris) - if err != nil { - return err - } - - model.Hosts = types.ListNull(types.StringType) - model.Uris = types.ListNull(types.StringType) - model.HttpAPIURIs = types.ListNull(types.StringType) - if credentials != nil { - if credentials.Hosts != nil { - respHosts := *credentials.Hosts - reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) - - hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) - if diags.HasError() { - return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) - } - - model.Hosts = hostsTF - } - model.Host = types.StringPointerValue(credentials.Host) - if credentials.HttpApiUris != nil { - respHttpApiUris := *credentials.HttpApiUris - - reconciledHttpApiUris := utils.ReconcileStringSlices(modelHttpApiUris, respHttpApiUris) - - httpApiUrisTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHttpApiUris) - if diags.HasError() { - return fmt.Errorf("failed to map httpApiUris: %w", core.DiagsToError(diags)) - } - - model.HttpAPIURIs = httpApiUrisTF - } - - if credentials.Uris != nil { - respUris := *credentials.Uris - - reconciledUris := utils.ReconcileStringSlices(modelUris, respUris) - - urisTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledUris) - if diags.HasError() { - return fmt.Errorf("failed to map uris: %w", core.DiagsToError(diags)) - } - - model.Uris = urisTF - } - - model.HttpAPIURI = types.StringPointerValue(credentials.HttpApiUri) - model.Management = types.StringPointerValue(credentials.Management) - model.Password = types.StringPointerValue(credentials.Password) - model.Port = types.Int64PointerValue(credentials.Port) - model.Uri = types.StringPointerValue(credentials.Uri) - model.Username = types.StringPointerValue(credentials.Username) - } - return nil -} diff --git a/stackit/internal/services/rabbitmq/credential/resource_test.go b/stackit/internal/services/rabbitmq/credential/resource_test.go deleted file mode 100644 index 5492d2fe..00000000 --- a/stackit/internal/services/rabbitmq/credential/resource_test.go +++ /dev/null @@ -1,280 +0,0 @@ -package rabbitmq - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *rabbitmq.CredentialsResponse - expected Model - isValid bool - }{ - { - "default_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &rabbitmq.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &rabbitmq.RawCredentials{}, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringNull(), - Hosts: types.ListNull(types.StringType), - HttpAPIURI: types.StringNull(), - HttpAPIURIs: types.ListNull(types.StringType), - Management: types.StringNull(), - Password: types.StringNull(), - Port: types.Int64Null(), - Uri: types.StringNull(), - Uris: types.ListNull(types.StringType), - Username: types.StringNull(), - }, - true, - }, - { - "simple_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &rabbitmq.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &rabbitmq.RawCredentials{ - Credentials: &rabbitmq.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "host_1", - "", - }, - HttpApiUri: utils.Ptr("http"), - HttpApiUris: &[]string{ - "http_api_uri_1", - "", - }, - Management: utils.Ptr("management"), - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - Uris: &[]string{ - "uri_1", - "", - }, - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_1"), - types.StringValue(""), - }), - HttpAPIURI: types.StringValue("http"), - HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("http_api_uri_1"), - types.StringValue(""), - }), - Management: types.StringValue("management"), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Uris: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("uri_1"), - types.StringValue(""), - }), - Username: types.StringValue("username"), - }, - true, - }, - { - "hosts_uris_unordered", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - Uris: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("uri_2"), - types.StringValue(""), - types.StringValue("uri_1"), - }), - HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("http_api_uri_2"), - types.StringValue(""), - types.StringValue("http_api_uri_1"), - }), - }, - &rabbitmq.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &rabbitmq.RawCredentials{ - Credentials: &rabbitmq.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "", - "host_1", - "host_2", - }, - HttpApiUri: utils.Ptr("http"), - HttpApiUris: &[]string{ - "", - "http_api_uri_1", - "http_api_uri_2", - }, - Management: utils.Ptr("management"), - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - Uris: &[]string{ - "", - "uri_1", - "uri_2", - }, - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - HttpAPIURI: types.StringValue("http"), - HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("http_api_uri_2"), - types.StringValue(""), - types.StringValue("http_api_uri_1"), - }), - Management: types.StringValue("management"), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Uris: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("uri_2"), - types.StringValue(""), - types.StringValue("uri_1"), - }), - Username: types.StringValue("username"), - }, - true, - }, - { - "null_fields_and_int_conversions", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &rabbitmq.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &rabbitmq.RawCredentials{ - Credentials: &rabbitmq.Credentials{ - Host: utils.Ptr(""), - Hosts: &[]string{}, - HttpApiUri: nil, - HttpApiUris: &[]string{}, - Management: nil, - Password: utils.Ptr(""), - Port: utils.Ptr(int64(2123456789)), - Uri: nil, - Uris: &[]string{}, - Username: utils.Ptr(""), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue(""), - Hosts: types.ListValueMust(types.StringType, []attr.Value{}), - HttpAPIURI: types.StringNull(), - HttpAPIURIs: types.ListValueMust(types.StringType, []attr.Value{}), - Management: types.StringNull(), - Password: types.StringValue(""), - Port: types.Int64Value(2123456789), - Uri: types.StringNull(), - Uris: types.ListValueMust(types.StringType, []attr.Value{}), - Username: types.StringValue(""), - }, - true, - }, - { - "nil_response", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &rabbitmq.CredentialsResponse{}, - Model{}, - false, - }, - { - "nil_raw_credential", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &rabbitmq.CredentialsResponse{ - Id: utils.Ptr("cid"), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/rabbitmq/instance/datasource.go b/stackit/internal/services/rabbitmq/instance/datasource.go deleted file mode 100644 index 7cd25641..00000000 --- a/stackit/internal/services/rabbitmq/instance/datasource.go +++ /dev/null @@ -1,260 +0,0 @@ -package rabbitmq - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *rabbitmq.APIClient -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_rabbitmq_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "RabbitMQ instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "RabbitMQ instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the RabbitMQ instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "consumer_timeout": "The timeout in milliseconds for the consumer.", - "enable_monitoring": "Enable monitoring.", - "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted.", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "plugins": "List of plugins to install. Must be a supported plugin name.", - "roles": "List of roles to assign to the instance.", - "syslog": "List of syslog servers to send logs to.", - "tls_ciphers": "List of TLS ciphers to use.", - "tls_protocols": "TLS protocol to use.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Computed: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Computed: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Computed: true, - }, - "consumer_timeout": schema.Int64Attribute{ - Description: parametersDescriptions["consumer_timeout"], - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Computed: true, - }, - "plugins": schema.ListAttribute{ - Description: parametersDescriptions["plugins"], - ElementType: types.StringType, - Computed: true, - }, - "roles": schema.ListAttribute{ - Description: parametersDescriptions["roles"], - ElementType: types.StringType, - Computed: true, - }, - "syslog": schema.ListAttribute{ - Description: parametersDescriptions["syslog"], - ElementType: types.StringType, - Computed: true, - }, - "tls_ciphers": schema.ListAttribute{ - Description: parametersDescriptions["tls_ciphers"], - ElementType: types.StringType, - Computed: true, - }, - "tls_protocols": schema.StringAttribute{ - Description: parametersDescriptions["tls_protocols"], - Computed: true, - }, - }, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - }, - "image_url": schema.StringAttribute{ - Computed: true, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "RabbitMQ instance read") -} diff --git a/stackit/internal/services/rabbitmq/instance/resource.go b/stackit/internal/services/rabbitmq/instance/resource.go deleted file mode 100644 index 10b8d340..00000000 --- a/stackit/internal/services/rabbitmq/instance/resource.go +++ /dev/null @@ -1,834 +0,0 @@ -package rabbitmq - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - CfGuid types.String `tfsdk:"cf_guid"` - CfSpaceGuid types.String `tfsdk:"cf_space_guid"` - DashboardUrl types.String `tfsdk:"dashboard_url"` - ImageUrl types.String `tfsdk:"image_url"` - Name types.String `tfsdk:"name"` - CfOrganizationGuid types.String `tfsdk:"cf_organization_guid"` - Parameters types.Object `tfsdk:"parameters"` - Version types.String `tfsdk:"version"` - PlanName types.String `tfsdk:"plan_name"` - PlanId types.String `tfsdk:"plan_id"` -} - -// Struct corresponding to DataSourceModel.Parameters -type parametersModel struct { - SgwAcl types.String `tfsdk:"sgw_acl"` - ConsumerTimeout types.Int64 `tfsdk:"consumer_timeout"` - EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` - Graphite types.String `tfsdk:"graphite"` - MaxDiskThreshold types.Int64 `tfsdk:"max_disk_threshold"` - MetricsFrequency types.Int64 `tfsdk:"metrics_frequency"` - MetricsPrefix types.String `tfsdk:"metrics_prefix"` - MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"` - Plugins types.List `tfsdk:"plugins"` - Roles types.List `tfsdk:"roles"` - Syslog types.List `tfsdk:"syslog"` - TlsCiphers types.List `tfsdk:"tls_ciphers"` - TlsProtocols types.String `tfsdk:"tls_protocols"` -} - -// Types corresponding to parametersModel -var parametersTypes = map[string]attr.Type{ - "sgw_acl": basetypes.StringType{}, - "consumer_timeout": basetypes.Int64Type{}, - "enable_monitoring": basetypes.BoolType{}, - "graphite": basetypes.StringType{}, - "max_disk_threshold": basetypes.Int64Type{}, - "metrics_frequency": basetypes.Int64Type{}, - "metrics_prefix": basetypes.StringType{}, - "monitoring_instance_id": basetypes.StringType{}, - "plugins": basetypes.ListType{ElemType: types.StringType}, - "roles": basetypes.ListType{ElemType: types.StringType}, - "syslog": basetypes.ListType{ElemType: types.StringType}, - "tls_ciphers": basetypes.ListType{ElemType: types.StringType}, - "tls_protocols": basetypes.StringType{}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *rabbitmq.APIClient -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_rabbitmq_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "RabbitMQ instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "RabbitMQ instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the RabbitMQ instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - "parameters": "Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "consumer_timeout": "The timeout in milliseconds for the consumer.", - "enable_monitoring": "Enable monitoring.", - "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted.", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "plugins": "List of plugins to install. Must be a supported plugin name.", - "roles": "List of roles to assign to the instance.", - "syslog": "List of syslog servers to send logs to.", - "tls_ciphers": "List of TLS ciphers to use.", - "tls_protocols": "TLS protocol to use.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Required: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Required: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Description: descriptions["parameters"], - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Optional: true, - Computed: true, - }, - "consumer_timeout": schema.Int64Attribute{ - Description: parametersDescriptions["consumer_timeout"], - Optional: true, - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Optional: true, - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Optional: true, - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Optional: true, - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Optional: true, - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Optional: true, - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "plugins": schema.ListAttribute{ - Description: parametersDescriptions["plugins"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "roles": schema.ListAttribute{ - Description: parametersDescriptions["roles"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "syslog": schema.ListAttribute{ - Description: parametersDescriptions["syslog"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "tls_ciphers": schema.ListAttribute{ - Description: parametersDescriptions["tls_ciphers"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "tls_protocols": schema.StringAttribute{ - Description: parametersDescriptions["tls_protocols"], - Optional: true, - Computed: true, - }, - }, - Optional: true, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "image_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.InstanceId - ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "RabbitMQ instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "RabbitMQ instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - err = r.client.PartialUpdateInstance(ctx, projectId, instanceId).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "RabbitMQ instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "RabbitMQ instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "RabbitMQ instance state imported") -} - -func mapFields(instance *rabbitmq.Instance, model *Model) error { - if instance == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.InstanceId != nil { - instanceId = *instance.InstanceId - } else { - return fmt.Errorf("instance id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) - model.InstanceId = types.StringValue(instanceId) - model.PlanId = types.StringPointerValue(instance.PlanId) - model.CfGuid = types.StringPointerValue(instance.CfGuid) - model.CfSpaceGuid = types.StringPointerValue(instance.CfSpaceGuid) - model.DashboardUrl = types.StringPointerValue(instance.DashboardUrl) - model.ImageUrl = types.StringPointerValue(instance.ImageUrl) - model.Name = types.StringPointerValue(instance.Name) - model.CfOrganizationGuid = types.StringPointerValue(instance.CfOrganizationGuid) - - if instance.Parameters == nil { - model.Parameters = types.ObjectNull(parametersTypes) - } else { - parameters, err := mapParameters(*instance.Parameters) - if err != nil { - return fmt.Errorf("mapping parameters: %w", err) - } - model.Parameters = parameters - } - return nil -} - -func mapParameters(params map[string]interface{}) (types.Object, error) { - attributes := map[string]attr.Value{} - for attribute := range parametersTypes { - var valueInterface interface{} - var ok bool - - // This replacement is necessary because Terraform does not allow hyphens in attribute names - // And the API uses hyphens in the attribute names (tls-ciphers, tls-protocols) - if attribute == "tls_ciphers" || attribute == "tls_protocols" { - alteredAttribute := strings.ReplaceAll(attribute, "_", "-") - valueInterface, ok = params[alteredAttribute] - } else { - valueInterface, ok = params[attribute] - } - if !ok { - // All fields are optional, so this is ok - // Set the value as nil, will be handled accordingly - valueInterface = nil - } - - var value attr.Value - switch parametersTypes[attribute].(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) - case basetypes.StringType: - if valueInterface == nil { - value = types.StringNull() - } else { - valueString, ok := valueInterface.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) - } - value = types.StringValue(valueString) - } - case basetypes.BoolType: - if valueInterface == nil { - value = types.BoolNull() - } else { - valueBool, ok := valueInterface.(bool) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) - } - value = types.BoolValue(valueBool) - } - case basetypes.Int64Type: - if valueInterface == nil { - value = types.Int64Null() - } else { - // This may be int64, int32, int or float64 - // We try to assert all 4 - var valueInt64 int64 - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) - case int64: - valueInt64 = temp - case int32: - valueInt64 = int64(temp) - case int: - valueInt64 = int64(temp) - case float64: - valueInt64 = int64(temp) - } - value = types.Int64Value(valueInt64) - } - case basetypes.ListType: // Assumed to be a list of strings - if valueInterface == nil { - value = types.ListNull(types.StringType) - } else { - // This may be []string{} or []interface{} - // We try to assert all 2 - var valueList []attr.Value - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) - case []string: - for _, x := range temp { - valueList = append(valueList, types.StringValue(x)) - } - case []interface{}: - for _, x := range temp { - xString, ok := x.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) - } - valueList = append(valueList, types.StringValue(xString)) - } - } - temp2, diags := types.ListValue(types.StringType, valueList) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) - } - value = temp2 - } - } - attributes[attribute] = value - } - - output, diags := types.ObjectValue(parametersTypes, attributes) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) - } - return output, nil -} - -func toCreatePayload(model *Model, parameters *parametersModel) (*rabbitmq.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("converting parameters: %w", err) - } - - return &rabbitmq.CreateInstancePayload{ - InstanceName: conversion.StringValueToPointer(model.Name), - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toUpdatePayload(model *Model, parameters *parametersModel) (*rabbitmq.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("converting parameters: %w", err) - } - - return &rabbitmq.PartialUpdateInstancePayload{ - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toInstanceParams(parameters *parametersModel) (*rabbitmq.InstanceParameters, error) { - if parameters == nil { - return nil, nil - } - payloadParams := &rabbitmq.InstanceParameters{} - - payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) - payloadParams.ConsumerTimeout = conversion.Int64ValueToPointer(parameters.ConsumerTimeout) - payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring) - payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite) - payloadParams.MaxDiskThreshold = conversion.Int64ValueToPointer(parameters.MaxDiskThreshold) - payloadParams.MetricsFrequency = conversion.Int64ValueToPointer(parameters.MetricsFrequency) - payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix) - payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) - payloadParams.TlsProtocols = rabbitmq.InstanceParametersGetTlsProtocolsAttributeType(conversion.StringValueToPointer(parameters.TlsProtocols)) - - var err error - payloadParams.Plugins, err = conversion.StringListToPointer(parameters.Plugins) - if err != nil { - return nil, fmt.Errorf("converting plugins: %w", err) - } - - payloadParams.Roles, err = conversion.StringListToPointer(parameters.Roles) - if err != nil { - return nil, fmt.Errorf("converting roles: %w", err) - } - - payloadParams.Syslog, err = conversion.StringListToPointer(parameters.Syslog) - if err != nil { - return nil, fmt.Errorf("converting syslog: %w", err) - } - - payloadParams.TlsCiphers, err = conversion.StringListToPointer(parameters.TlsCiphers) - if err != nil { - return nil, fmt.Errorf("converting tls_ciphers: %w", err) - } - - return payloadParams, nil -} - -func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { - projectId := model.ProjectId.ValueString() - res, err := r.client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting RabbitMQ offerings: %w", err) - } - - version := model.Version.ValueString() - planName := model.PlanName.ValueString() - availableVersions := "" - availablePlanNames := "" - isValidVersion := false - for _, offer := range *res.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) - continue - } - isValidVersion = true - - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue - } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - model.PlanId = types.StringPointerValue(plan.Id) - return nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) - } - } - - if !isValidVersion { - return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) - } - return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) -} - -func loadPlanNameAndVersion(ctx context.Context, client *rabbitmq.APIClient, model *Model) error { - projectId := model.ProjectId.ValueString() - planId := model.PlanId.ValueString() - res, err := client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting RabbitMQ offerings: %w", err) - } - - for _, offer := range *res.Offerings { - for _, plan := range *offer.Plans { - if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { - model.PlanName = types.StringPointerValue(plan.Name) - model.Version = types.StringPointerValue(offer.Version) - return nil - } - } - } - - return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) -} diff --git a/stackit/internal/services/rabbitmq/instance/resource_test.go b/stackit/internal/services/rabbitmq/instance/resource_test.go deleted file mode 100644 index 1b42d27c..00000000 --- a/stackit/internal/services/rabbitmq/instance/resource_test.go +++ /dev/null @@ -1,354 +0,0 @@ -package rabbitmq - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" -) - -var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringValue("acl"), - "consumer_timeout": types.Int64Value(10), - "enable_monitoring": types.BoolValue(true), - "graphite": types.StringValue("1.1.1.1:91"), - "max_disk_threshold": types.Int64Value(100), - "metrics_frequency": types.Int64Value(10), - "metrics_prefix": types.StringValue("prefix"), - "monitoring_instance_id": types.StringValue("mid"), - "plugins": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("plugin1"), - types.StringValue("plugin2"), - }), - "roles": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("role1"), - types.StringValue("role2"), - }), - "syslog": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("syslog"), - types.StringValue("syslog2"), - }), - "tls_ciphers": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ciphers1"), - types.StringValue("ciphers2"), - }), - "tls_protocols": types.StringValue(string(rabbitmq.INSTANCEPARAMETERSTLS_PROTOCOLS__2)), -}) - -var fixtureInstanceParameters = rabbitmq.InstanceParameters{ - SgwAcl: utils.Ptr("acl"), - ConsumerTimeout: utils.Ptr(int64(10)), - EnableMonitoring: utils.Ptr(true), - Graphite: utils.Ptr("1.1.1.1:91"), - MaxDiskThreshold: utils.Ptr(int64(100)), - MetricsFrequency: utils.Ptr(int64(10)), - MetricsPrefix: utils.Ptr("prefix"), - MonitoringInstanceId: utils.Ptr("mid"), - Plugins: &[]string{"plugin1", "plugin2"}, - Roles: &[]string{"role1", "role2"}, - Syslog: &[]string{"syslog", "syslog2"}, - TlsCiphers: &[]string{"ciphers1", "ciphers2"}, - TlsProtocols: rabbitmq.INSTANCEPARAMETERSTLS_PROTOCOLS__2.Ptr(), -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *rabbitmq.Instance - expected Model - isValid bool - }{ - { - "default_values", - &rabbitmq.Instance{}, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringNull(), - Name: types.StringNull(), - CfGuid: types.StringNull(), - CfSpaceGuid: types.StringNull(), - DashboardUrl: types.StringNull(), - ImageUrl: types.StringNull(), - CfOrganizationGuid: types.StringNull(), - Parameters: types.ObjectNull(parametersTypes), - }, - true, - }, - { - "simple_values", - &rabbitmq.Instance{ - PlanId: utils.Ptr("plan"), - CfGuid: utils.Ptr("cf"), - CfSpaceGuid: utils.Ptr("space"), - DashboardUrl: utils.Ptr("dashboard"), - ImageUrl: utils.Ptr("image"), - InstanceId: utils.Ptr("iid"), - Name: utils.Ptr("name"), - CfOrganizationGuid: utils.Ptr("org"), - Parameters: &map[string]interface{}{ - "sgw_acl": "acl", - "consumer_timeout": 10, - "enable_monitoring": true, - "graphite": "1.1.1.1:91", - "max_disk_threshold": 100, - "metrics_frequency": 10, - "metrics_prefix": "prefix", - "monitoring_instance_id": "mid", - "plugins": []string{"plugin1", "plugin2"}, - "roles": []string{"role1", "role2"}, - "syslog": []string{"syslog", "syslog2"}, - "tls-ciphers": []string{"ciphers1", "ciphers2"}, - "tls-protocols": string(rabbitmq.INSTANCEPARAMETERSTLS_PROTOCOLS__2), - }, - }, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringValue("plan"), - Name: types.StringValue("name"), - CfGuid: types.StringValue("cf"), - CfSpaceGuid: types.StringValue("space"), - DashboardUrl: types.StringValue("dashboard"), - ImageUrl: types.StringValue("image"), - CfOrganizationGuid: types.StringValue("org"), - Parameters: fixtureModelParameters, - }, - true, - }, - - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &rabbitmq.Instance{}, - Model{}, - false, - }, - { - "wrong_param_types_1", - &rabbitmq.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": true, - }, - }, - Model{}, - false, - }, - { - "wrong_param_types_2", - &rabbitmq.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": 1, - }, - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *rabbitmq.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &rabbitmq.CreateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &rabbitmq.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - PlanId: types.StringValue(""), - Parameters: fixtureModelParameters, - }, - &rabbitmq.CreateInstancePayload{ - InstanceName: utils.Ptr(""), - PlanId: utils.Ptr(""), - Parameters: &fixtureInstanceParameters, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - }, - &rabbitmq.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toCreatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *rabbitmq.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &rabbitmq.PartialUpdateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &rabbitmq.PartialUpdateInstancePayload{ - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - PlanId: types.StringValue(""), - Parameters: fixtureModelParameters, - }, - &rabbitmq.PartialUpdateInstancePayload{ - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - PlanId: types.StringValue("plan"), - }, - &rabbitmq.PartialUpdateInstancePayload{ - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toUpdatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go b/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go deleted file mode 100644 index ff9d3f41..00000000 --- a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go +++ /dev/null @@ -1,312 +0,0 @@ -package rabbitmq_test - -import ( - "context" - "fmt" - "regexp" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// Instance resource data -var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": testutil.ResourceNameWithDateTime("rabbitmq"), - "plan_id": "6af42a95-8b68-436d-907b-8ae37dfec52b", - "plan_name": "stackit-rabbitmq-2.4.10-single", - "version": "3.13", - "sgw_acl_invalid": "1.2.3.4/4", - "sgw_acl_valid": "192.168.0.0/16", -} - -func parametersConfig(params map[string]string) string { - nonStringParams := []string{ - "consumer_timeout", - "enable_monitoring", - "max_disk_threshold", - "metrics_frequency", - "plugins", - "roles", - "syslog", - "tls_ciphers", - } - parameters := "parameters = {" - for k, v := range params { - if utils.Contains(nonStringParams, k) { - parameters += fmt.Sprintf("%s = %s\n", k, v) - } else { - parameters += fmt.Sprintf("%s = %q\n", k, v) - } - } - parameters += "\n}" - return parameters -} - -func resourceConfig(params map[string]string) string { - return fmt.Sprintf(` - %s - - resource "stackit_rabbitmq_instance" "instance" { - project_id = "%s" - name = "%s" - plan_name = "%s" - version = "%s" - %s - } - - %s - `, - testutil.RabbitMQProviderConfig(), - instanceResource["project_id"], - instanceResource["name"], - instanceResource["plan_name"], - instanceResource["version"], - parametersConfig(params), - resourceConfigCredential(), - ) -} - -func resourceConfigCredential() string { - return ` - resource "stackit_rabbitmq_credential" "credential" { - project_id = stackit_rabbitmq_instance.instance.project_id - instance_id = stackit_rabbitmq_instance.instance.instance_id - } - ` -} - -func TestAccRabbitMQResource(t *testing.T) { - acls := instanceResource["sgw_acl_invalid"] - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckRabbitMQDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: resourceConfig(map[string]string{"sgw_acl": acls}), - ExpectError: regexp.MustCompile(`.*sgw_acl is invalid.*`), - }, - // Creation - { - Config: resourceConfig(map[string]string{ - "sgw_acl": instanceResource["sgw_acl_valid"], - "consumer_timeout": "1800000", - "enable_monitoring": "false", - "graphite": "graphite.example.com:2003", - "max_disk_threshold": "80", - "metrics_frequency": "60", - "metrics_prefix": "rabbitmq", - "plugins": `["rabbitmq_federation"]`, - "roles": `["administrator"]`, - "syslog": `["syslog.example.com:514"]`, - "tls_ciphers": `["TLS_AES_128_GCM_SHA256"]`, - }), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_rabbitmq_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "plan_name", instanceResource["plan_name"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "name", instanceResource["name"]), - - // Instance params data - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.consumer_timeout", "1800000"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.enable_monitoring", "false"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.graphite", "graphite.example.com:2003"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.max_disk_threshold", "80"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.metrics_frequency", "60"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.metrics_prefix", "rabbitmq"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.plugins.#", "1"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.plugins.0", "rabbitmq_federation"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.roles.#", "1"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.roles.0", "administrator"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.syslog.#", "1"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.syslog.0", "syslog.example.com:514"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.tls_ciphers.#", "1"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.tls_ciphers.0", "TLS_AES_128_GCM_SHA256"), - - // Credential data - resource.TestCheckResourceAttrPair( - "stackit_rabbitmq_credential.credential", "project_id", - "stackit_rabbitmq_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_rabbitmq_credential.credential", "instance_id", - "stackit_rabbitmq_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_rabbitmq_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_rabbitmq_credential.credential", "host"), - ), - }, - // data source - { - Config: fmt.Sprintf(` - %s - - data "stackit_rabbitmq_instance" "instance" { - project_id = stackit_rabbitmq_instance.instance.project_id - instance_id = stackit_rabbitmq_instance.instance.instance_id - } - - data "stackit_rabbitmq_credential" "credential" { - project_id = stackit_rabbitmq_credential.credential.project_id - instance_id = stackit_rabbitmq_credential.credential.instance_id - credential_id = stackit_rabbitmq_credential.credential.credential_id - }`, - resourceConfig(nil), - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_rabbitmq_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrPair("stackit_rabbitmq_instance.instance", "instance_id", - "data.stackit_rabbitmq_credential.credential", "instance_id"), - resource.TestCheckResourceAttrPair("data.stackit_rabbitmq_instance.instance", "instance_id", - "data.stackit_rabbitmq_credential.credential", "instance_id"), - resource.TestCheckResourceAttr("data.stackit_rabbitmq_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("data.stackit_rabbitmq_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttrSet("data.stackit_rabbitmq_instance.instance", "parameters.sgw_acl"), - - // Credential data - resource.TestCheckResourceAttr("data.stackit_rabbitmq_credential.credential", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("data.stackit_rabbitmq_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("data.stackit_rabbitmq_credential.credential", "host"), - resource.TestCheckResourceAttrSet("data.stackit_rabbitmq_credential.credential", "port"), - resource.TestCheckResourceAttrSet("data.stackit_rabbitmq_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("data.stackit_rabbitmq_credential.credential", "management"), - resource.TestCheckResourceAttrSet("data.stackit_rabbitmq_credential.credential", "http_api_uri"), - ), - }, - // Import - { - ResourceName: "stackit_rabbitmq_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_rabbitmq_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_rabbitmq_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "stackit_rabbitmq_credential.credential", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_rabbitmq_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_rabbitmq_credential.credential") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_valid"]}), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_rabbitmq_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "plan_name", instanceResource["plan_name"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_rabbitmq_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid"]), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckRabbitMQDestroy(s *terraform.State) error { - ctx := context.Background() - var client *rabbitmq.APIClient - var err error - if testutil.RabbitMQCustomEndpoint == "" { - client, err = rabbitmq.NewAPIClient( - config.WithRegion("eu01"), - ) - } else { - client, err = rabbitmq.NewAPIClient( - config.WithEndpoint(testutil.RabbitMQCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_rabbitmq_instance" { - continue - } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := *instancesResp.Instances - for i := range instances { - if instances[i].InstanceId == nil { - continue - } - if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { - if !checkInstanceDeleteSuccess(&instances[i]) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) - } - } - } - } - return nil -} - -func checkInstanceDeleteSuccess(i *rabbitmq.Instance) bool { - if *i.LastOperation.Type != rabbitmq.INSTANCELASTOPERATIONTYPE_DELETE { - return false - } - - if *i.LastOperation.Type == rabbitmq.INSTANCELASTOPERATIONTYPE_DELETE { - if *i.LastOperation.State != rabbitmq.INSTANCELASTOPERATIONSTATE_SUCCEEDED { - return false - } else if strings.Contains(*i.LastOperation.Description, "DeleteFailed") || strings.Contains(*i.LastOperation.Description, "failed") { - return false - } - } - return true -} diff --git a/stackit/internal/services/rabbitmq/utils/util.go b/stackit/internal/services/rabbitmq/utils/util.go deleted file mode 100644 index 1f7a1c09..00000000 --- a/stackit/internal/services/rabbitmq/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *rabbitmq.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.RabbitMQCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.RabbitMQCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := rabbitmq.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/rabbitmq/utils/util_test.go b/stackit/internal/services/rabbitmq/utils/util_test.go deleted file mode 100644 index 105f71d9..00000000 --- a/stackit/internal/services/rabbitmq/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://rabbitmq-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *rabbitmq.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *rabbitmq.APIClient { - apiClient, err := rabbitmq.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - RabbitMQCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *rabbitmq.APIClient { - apiClient, err := rabbitmq.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/redis/credential/datasource.go b/stackit/internal/services/redis/credential/datasource.go deleted file mode 100644 index d27ff1e2..00000000 --- a/stackit/internal/services/redis/credential/datasource.go +++ /dev/null @@ -1,179 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/redis" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &credentialDataSource{} -) - -// NewCredentialDataSource is a helper function to simplify the provider implementation. -func NewCredentialDataSource() datasource.DataSource { - return &credentialDataSource{} -} - -// credentialDataSource is the data source implementation. -type credentialDataSource struct { - client *redis.APIClient -} - -// Metadata returns the data source type name. -func (r *credentialDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_redis_credential" -} - -// Configure adds the provider configured client to the data source. -func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Redis credential client configured") -} - -// Schema defines the schema for the data source. -func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Redis credential data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the Redis instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "uri": "Connection URI.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "load_balanced_host": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Description: descriptions["uri"], - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading credential", - fmt.Sprintf("Credential with ID %q or instance with ID %q does not exist in project %q.", credentialId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Redis credential read") -} diff --git a/stackit/internal/services/redis/credential/resource.go b/stackit/internal/services/redis/credential/resource.go deleted file mode 100644 index 11641c97..00000000 --- a/stackit/internal/services/redis/credential/resource.go +++ /dev/null @@ -1,373 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &credentialResource{} - _ resource.ResourceWithConfigure = &credentialResource{} - _ resource.ResourceWithImportState = &credentialResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - CredentialId types.String `tfsdk:"credential_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Host types.String `tfsdk:"host"` - Hosts types.List `tfsdk:"hosts"` - LoadBalancedHost types.String `tfsdk:"load_balanced_host"` - Password types.String `tfsdk:"password"` - Port types.Int64 `tfsdk:"port"` - Uri types.String `tfsdk:"uri"` - Username types.String `tfsdk:"username"` -} - -// NewCredentialResource is a helper function to simplify the provider implementation. -func NewCredentialResource() resource.Resource { - return &credentialResource{} -} - -// credentialResource is the resource implementation. -type credentialResource struct { - client *redis.APIClient -} - -// Metadata returns the resource type name. -func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_redis_credential" -} - -// Configure adds the provider configured client to the resource. -func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Redis credential client configured") -} - -// Schema defines the schema for the resource. -func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Redis credential resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`credential_id`\".", - "credential_id": "The credential's ID.", - "instance_id": "ID of the Redis instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "uri": "Connection URI.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "credential_id": schema.StringAttribute{ - Description: descriptions["credential_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "hosts": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "load_balanced_host": schema.StringAttribute{ - Computed: true, - }, - "password": schema.StringAttribute{ - Computed: true, - Sensitive: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "uri": schema.StringAttribute{ - Description: descriptions["uri"], - Computed: true, - Sensitive: true, - }, - "username": schema.StringAttribute{ - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Create new recordset - credentialsResp, err := r.client.CreateCredentials(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if credentialsResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") - return - } - credentialId := *credentialsResp.Id - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - waitResp, err := wait.CreateCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Redis credential created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - recordSetResp, err := r.client.GetCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, recordSetResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Redis credential read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - credentialId := model.CredentialId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "credential_id", credentialId) - - // Delete existing record set - err := r.client.DeleteCredentials(ctx, projectId, instanceId, credentialId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "Redis credential deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id -func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("credential_id"), idParts[2])...) - tflog.Info(ctx, "Redis credential state imported") -} - -func mapFields(ctx context.Context, credentialsResp *redis.CredentialsResponse, model *Model) error { - if credentialsResp == nil { - return fmt.Errorf("response input is nil") - } - if credentialsResp.Raw == nil { - return fmt.Errorf("response credentials raw is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - credentials := credentialsResp.Raw.Credentials - - var credentialId string - if model.CredentialId.ValueString() != "" { - credentialId = model.CredentialId.ValueString() - } else if credentialsResp.Id != nil { - credentialId = *credentialsResp.Id - } else { - return fmt.Errorf("credentials id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId) - - modelHosts, err := utils.ListValuetoStringSlice(model.Hosts) - if err != nil { - return err - } - - model.CredentialId = types.StringValue(credentialId) - model.Hosts = types.ListNull(types.StringType) - if credentials != nil { - if credentials.Hosts != nil { - respHosts := *credentials.Hosts - - reconciledHosts := utils.ReconcileStringSlices(modelHosts, respHosts) - - hostsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledHosts) - if diags.HasError() { - return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) - } - - model.Hosts = hostsTF - } - model.Host = types.StringPointerValue(credentials.Host) - model.LoadBalancedHost = types.StringPointerValue(credentials.LoadBalancedHost) - model.Password = types.StringPointerValue(credentials.Password) - model.Port = types.Int64PointerValue(credentials.Port) - model.Uri = types.StringPointerValue(credentials.Uri) - model.Username = types.StringPointerValue(credentials.Username) - } - return nil -} diff --git a/stackit/internal/services/redis/credential/resource_test.go b/stackit/internal/services/redis/credential/resource_test.go deleted file mode 100644 index d4d1c641..00000000 --- a/stackit/internal/services/redis/credential/resource_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package redis - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - state Model - input *redis.CredentialsResponse - expected Model - isValid bool - }{ - { - "default_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &redis.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &redis.RawCredentials{}, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringNull(), - Hosts: types.ListNull(types.StringType), - LoadBalancedHost: types.StringNull(), - Password: types.StringNull(), - Port: types.Int64Null(), - Uri: types.StringNull(), - Username: types.StringNull(), - }, - true, - }, - { - "simple_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &redis.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &redis.RawCredentials{ - Credentials: &redis.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "host_1", - "", - }, - LoadBalancedHost: utils.Ptr("load_balanced_host"), - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_1"), - types.StringValue(""), - }), - LoadBalancedHost: types.StringValue("load_balanced_host"), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Username: types.StringValue("username"), - }, - true, - }, - { - "hosts_unordered", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - }, - &redis.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &redis.RawCredentials{ - Credentials: &redis.Credentials{ - Host: utils.Ptr("host"), - Hosts: &[]string{ - "", - "host_1", - "host_2", - }, - LoadBalancedHost: utils.Ptr("load_balanced_host"), - Password: utils.Ptr("password"), - Port: utils.Ptr(int64(1234)), - Uri: utils.Ptr("uri"), - Username: utils.Ptr("username"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue("host"), - Hosts: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("host_2"), - types.StringValue(""), - types.StringValue("host_1"), - }), - LoadBalancedHost: types.StringValue("load_balanced_host"), - Password: types.StringValue("password"), - Port: types.Int64Value(1234), - Uri: types.StringValue("uri"), - Username: types.StringValue("username"), - }, - true, - }, - { - "null_fields_and_int_conversions", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &redis.CredentialsResponse{ - Id: utils.Ptr("cid"), - Raw: &redis.RawCredentials{ - Credentials: &redis.Credentials{ - Host: utils.Ptr(""), - Hosts: &[]string{}, - LoadBalancedHost: nil, - Password: utils.Ptr(""), - Port: utils.Ptr(int64(2123456789)), - Uri: nil, - Username: utils.Ptr(""), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid,cid"), - CredentialId: types.StringValue("cid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Host: types.StringValue(""), - Hosts: types.ListValueMust(types.StringType, []attr.Value{}), - LoadBalancedHost: types.StringNull(), - Password: types.StringValue(""), - Port: types.Int64Value(2123456789), - Uri: types.StringNull(), - Username: types.StringValue(""), - }, - true, - }, - { - "nil_response", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - nil, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &redis.CredentialsResponse{}, - Model{}, - false, - }, - { - "nil_raw_credential", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &redis.CredentialsResponse{ - Id: utils.Ptr("cid"), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/redis/instance/datasource.go b/stackit/internal/services/redis/instance/datasource.go deleted file mode 100644 index d9e4d460..00000000 --- a/stackit/internal/services/redis/instance/datasource.go +++ /dev/null @@ -1,309 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/redis" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *redis.APIClient -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_redis_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Redis instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Redis instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. identifier. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the Redis instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "down_after_milliseconds": "The number of milliseconds after which the instance is considered down.", - "enable_monitoring": "Enable monitoring.", - "failover_timeout": "The failover timeout in milliseconds.", - "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.", - "lazyfree_lazy_eviction": "The lazy eviction enablement (yes or no).", - "lazyfree_lazy_expire": "The lazy expire enablement (yes or no).", - "lua_time_limit": "The Lua time limit.", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "maxclients": "The maximum number of clients.", - "maxmemory_policy": "The policy to handle the maximum memory (volatile-lru, noeviction, etc).", - "maxmemory_samples": "The maximum memory samples.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted.", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", - "min_replicas_max_lag": "The minimum replicas maximum lag.", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "notify_keyspace_events": "The notify keyspace events.", - "snapshot": "The snapshot configuration.", - "syslog": "List of syslog servers to send logs to.", - "tls_ciphers": "List of TLS ciphers to use.", - "tls_ciphersuites": "TLS cipher suites to use.", - "tls_protocols": "TLS protocol to use.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Computed: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Computed: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Computed: true, - }, - "down_after_milliseconds": schema.Int64Attribute{ - Description: parametersDescriptions["down_after_milliseconds"], - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Computed: true, - }, - "failover_timeout": schema.Int64Attribute{ - Description: parametersDescriptions["failover_timeout"], - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Computed: true, - }, - "lazyfree_lazy_eviction": schema.StringAttribute{ - Description: parametersDescriptions["lazyfree_lazy_eviction"], - Computed: true, - }, - "lazyfree_lazy_expire": schema.StringAttribute{ - Description: parametersDescriptions["lazyfree_lazy_expire"], - Computed: true, - }, - "lua_time_limit": schema.Int64Attribute{ - Description: parametersDescriptions["lua_time_limit"], - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Computed: true, - }, - "maxclients": schema.Int64Attribute{ - Description: parametersDescriptions["maxclients"], - Computed: true, - }, - "maxmemory_policy": schema.StringAttribute{ - Description: parametersDescriptions["maxmemory_policy"], - Computed: true, - }, - "maxmemory_samples": schema.Int64Attribute{ - Description: parametersDescriptions["maxmemory_samples"], - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Computed: true, - }, - "min_replicas_max_lag": schema.Int64Attribute{ - Description: parametersDescriptions["min_replicas_max_lag"], - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Computed: true, - }, - "notify_keyspace_events": schema.StringAttribute{ - Description: parametersDescriptions["notify_keyspace_events"], - Computed: true, - }, - "snapshot": schema.StringAttribute{ - Description: parametersDescriptions["snapshot"], - Computed: true, - }, - "syslog": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["syslog"], - Computed: true, - }, - "tls_ciphers": schema.ListAttribute{ - ElementType: types.StringType, - Description: parametersDescriptions["tls_ciphers"], - Computed: true, - }, - "tls_ciphersuites": schema.StringAttribute{ - Description: parametersDescriptions["tls_ciphersuites"], - Computed: true, - }, - "tls_protocols": schema.StringAttribute{ - Description: parametersDescriptions["tls_protocols"], - Computed: true, - }, - }, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Description: descriptions["cf_guid"], - Computed: true, - }, - "cf_space_guid": schema.StringAttribute{ - Description: descriptions["cf_space_guid"], - Computed: true, - }, - "dashboard_url": schema.StringAttribute{ - Description: descriptions["dashboard_url"], - Computed: true, - }, - "image_url": schema.StringAttribute{ - Description: descriptions["image_url"], - Computed: true, - }, - "cf_organization_guid": schema.StringAttribute{ - Description: descriptions["cf_organization_guid"], - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - http.StatusGone: fmt.Sprintf("Instance %q is gone.", instanceId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Redis instance read") -} diff --git a/stackit/internal/services/redis/instance/resource.go b/stackit/internal/services/redis/instance/resource.go deleted file mode 100644 index 1dbb407b..00000000 --- a/stackit/internal/services/redis/instance/resource.go +++ /dev/null @@ -1,920 +0,0 @@ -package redis - -import ( - "context" - "fmt" - "net/http" - "slices" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - CfGuid types.String `tfsdk:"cf_guid"` - CfSpaceGuid types.String `tfsdk:"cf_space_guid"` - DashboardUrl types.String `tfsdk:"dashboard_url"` - ImageUrl types.String `tfsdk:"image_url"` - Name types.String `tfsdk:"name"` - CfOrganizationGuid types.String `tfsdk:"cf_organization_guid"` - Parameters types.Object `tfsdk:"parameters"` - Version types.String `tfsdk:"version"` - PlanName types.String `tfsdk:"plan_name"` - PlanId types.String `tfsdk:"plan_id"` -} - -// Struct corresponding to DataSourceModel.Parameters -type parametersModel struct { - SgwAcl types.String `tfsdk:"sgw_acl"` - DownAfterMilliseconds types.Int64 `tfsdk:"down_after_milliseconds"` - EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` - FailoverTimeout types.Int64 `tfsdk:"failover_timeout"` - Graphite types.String `tfsdk:"graphite"` - LazyfreeLazyEviction types.String `tfsdk:"lazyfree_lazy_eviction"` - LazyfreeLazyExpire types.String `tfsdk:"lazyfree_lazy_expire"` - LuaTimeLimit types.Int64 `tfsdk:"lua_time_limit"` - MaxDiskThreshold types.Int64 `tfsdk:"max_disk_threshold"` - Maxclients types.Int64 `tfsdk:"maxclients"` - MaxmemoryPolicy types.String `tfsdk:"maxmemory_policy"` - MaxmemorySamples types.Int64 `tfsdk:"maxmemory_samples"` - MetricsFrequency types.Int64 `tfsdk:"metrics_frequency"` - MetricsPrefix types.String `tfsdk:"metrics_prefix"` - MinReplicasMaxLag types.Int64 `tfsdk:"min_replicas_max_lag"` - MonitoringInstanceId types.String `tfsdk:"monitoring_instance_id"` - NotifyKeyspaceEvents types.String `tfsdk:"notify_keyspace_events"` - Snapshot types.String `tfsdk:"snapshot"` - Syslog types.List `tfsdk:"syslog"` - TlsCiphers types.List `tfsdk:"tls_ciphers"` - TlsCiphersuites types.String `tfsdk:"tls_ciphersuites"` - TlsProtocols types.String `tfsdk:"tls_protocols"` -} - -// Types corresponding to parametersModel -var parametersTypes = map[string]attr.Type{ - "sgw_acl": basetypes.StringType{}, - "down_after_milliseconds": basetypes.Int64Type{}, - "enable_monitoring": basetypes.BoolType{}, - "failover_timeout": basetypes.Int64Type{}, - "graphite": basetypes.StringType{}, - "lazyfree_lazy_eviction": basetypes.StringType{}, - "lazyfree_lazy_expire": basetypes.StringType{}, - "lua_time_limit": basetypes.Int64Type{}, - "max_disk_threshold": basetypes.Int64Type{}, - "maxclients": basetypes.Int64Type{}, - "maxmemory_policy": basetypes.StringType{}, - "maxmemory_samples": basetypes.Int64Type{}, - "metrics_frequency": basetypes.Int64Type{}, - "metrics_prefix": basetypes.StringType{}, - "min_replicas_max_lag": basetypes.Int64Type{}, - "monitoring_instance_id": basetypes.StringType{}, - "notify_keyspace_events": basetypes.StringType{}, - "snapshot": basetypes.StringType{}, - "syslog": basetypes.ListType{ElemType: types.StringType}, - "tls_ciphers": basetypes.ListType{ElemType: types.StringType}, - "tls_ciphersuites": basetypes.StringType{}, - "tls_protocols": basetypes.StringType{}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *redis.APIClient -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_redis_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Redis instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Redis instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the Redis instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "version": "The service version.", - "plan_name": "The selected plan name.", - "plan_id": "The selected plan ID.", - "parameters": "Configuration parameters. Please note that removing a previously configured field from your Terraform configuration won't replace its value in the API. To update a previously configured field, explicitly set a new value for it.", - } - - parametersDescriptions := map[string]string{ - "sgw_acl": "Comma separated list of IP networks in CIDR notation which are allowed to access this instance.", - "down_after_milliseconds": "The number of milliseconds after which the instance is considered down.", - "enable_monitoring": "Enable monitoring.", - "failover_timeout": "The failover timeout in milliseconds.", - "graphite": "Graphite server URL (host and port). If set, monitoring with Graphite will be enabled.", - "lazyfree_lazy_eviction": "The lazy eviction enablement (yes or no).", - "lazyfree_lazy_expire": "The lazy expire enablement (yes or no).", - "lua_time_limit": "The Lua time limit.", - "max_disk_threshold": "The maximum disk threshold in MB. If the disk usage exceeds this threshold, the instance will be stopped.", - "maxclients": "The maximum number of clients.", - "maxmemory_policy": "The policy to handle the maximum memory (volatile-lru, noeviction, etc).", - "maxmemory_samples": "The maximum memory samples.", - "metrics_frequency": "The frequency in seconds at which metrics are emitted.", - "metrics_prefix": "The prefix for the metrics. Could be useful when using Graphite monitoring to prefix the metrics with a certain value, like an API key", - "min_replicas_max_lag": "The minimum replicas maximum lag.", - "monitoring_instance_id": "The ID of the STACKIT monitoring instance.", - "notify_keyspace_events": "The notify keyspace events.", - "snapshot": "The snapshot configuration.", - "syslog": "List of syslog servers to send logs to.", - "tls_ciphers": "List of TLS ciphers to use.", - "tls_ciphersuites": "TLS cipher suites to use.", - "tls_protocols": "TLS protocol to use.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "version": schema.StringAttribute{ - Description: descriptions["version"], - Required: true, - }, - "plan_name": schema.StringAttribute{ - Description: descriptions["plan_name"], - Required: true, - }, - "plan_id": schema.StringAttribute{ - Description: descriptions["plan_id"], - Computed: true, - }, - "parameters": schema.SingleNestedAttribute{ - Description: descriptions["parameters"], - Attributes: map[string]schema.Attribute{ - "sgw_acl": schema.StringAttribute{ - Description: parametersDescriptions["sgw_acl"], - Optional: true, - Computed: true, - }, - "down_after_milliseconds": schema.Int64Attribute{ - Description: parametersDescriptions["down_after_milliseconds"], - Optional: true, - Computed: true, - }, - "enable_monitoring": schema.BoolAttribute{ - Description: parametersDescriptions["enable_monitoring"], - Optional: true, - Computed: true, - }, - "failover_timeout": schema.Int64Attribute{ - Description: parametersDescriptions["failover_timeout"], - Optional: true, - Computed: true, - }, - "graphite": schema.StringAttribute{ - Description: parametersDescriptions["graphite"], - Optional: true, - Computed: true, - }, - "lazyfree_lazy_eviction": schema.StringAttribute{ - Description: parametersDescriptions["lazyfree_lazy_eviction"], - Optional: true, - Computed: true, - }, - "lazyfree_lazy_expire": schema.StringAttribute{ - Description: parametersDescriptions["lazyfree_lazy_expire"], - Optional: true, - Computed: true, - }, - "lua_time_limit": schema.Int64Attribute{ - Description: parametersDescriptions["lua_time_limit"], - Optional: true, - Computed: true, - }, - "max_disk_threshold": schema.Int64Attribute{ - Description: parametersDescriptions["max_disk_threshold"], - Optional: true, - Computed: true, - }, - "maxclients": schema.Int64Attribute{ - Description: parametersDescriptions["maxclients"], - Optional: true, - Computed: true, - }, - "maxmemory_policy": schema.StringAttribute{ - Description: parametersDescriptions["maxmemory_policy"], - Optional: true, - Computed: true, - }, - "maxmemory_samples": schema.Int64Attribute{ - Description: parametersDescriptions["maxmemory_samples"], - Optional: true, - Computed: true, - }, - "metrics_frequency": schema.Int64Attribute{ - Description: parametersDescriptions["metrics_frequency"], - Optional: true, - Computed: true, - }, - "metrics_prefix": schema.StringAttribute{ - Description: parametersDescriptions["metrics_prefix"], - Optional: true, - Computed: true, - }, - "min_replicas_max_lag": schema.Int64Attribute{ - Description: parametersDescriptions["min_replicas_max_lag"], - Optional: true, - Computed: true, - }, - "monitoring_instance_id": schema.StringAttribute{ - Description: parametersDescriptions["monitoring_instance_id"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "notify_keyspace_events": schema.StringAttribute{ - Description: parametersDescriptions["notify_keyspace_events"], - Optional: true, - Computed: true, - }, - "snapshot": schema.StringAttribute{ - Description: parametersDescriptions["snapshot"], - Optional: true, - Computed: true, - }, - "syslog": schema.ListAttribute{ - Description: parametersDescriptions["syslog"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "tls_ciphers": schema.ListAttribute{ - Description: parametersDescriptions["tls_ciphers"], - ElementType: types.StringType, - Optional: true, - Computed: true, - }, - "tls_ciphersuites": schema.StringAttribute{ - Description: parametersDescriptions["tls_ciphersuites"], - Optional: true, - Computed: true, - }, - "tls_protocols": schema.StringAttribute{ - Description: parametersDescriptions["tls_protocols"], - Optional: true, - Computed: true, - }, - }, - Optional: true, - Computed: true, - }, - "cf_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_space_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "dashboard_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "image_url": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cf_organization_guid": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.InstanceId - ctx = tflog.SetField(ctx, "instance_id", instanceId) - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Redis instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(instanceResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Compute and store values not present in the API response - err = loadPlanNameAndVersion(ctx, r.client, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Loading service plan details: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Redis instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err := r.loadPlanId(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading service plan: %v", err)) - return - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, parameters) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - err = r.client.PartialUpdateInstance(ctx, projectId, instanceId).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(waitResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Redis instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "Redis instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "Redis instance state imported") -} - -func mapFields(instance *redis.Instance, model *Model) error { - if instance == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.InstanceId != nil { - instanceId = *instance.InstanceId - } else { - return fmt.Errorf("instance id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) - model.InstanceId = types.StringValue(instanceId) - model.PlanId = types.StringPointerValue(instance.PlanId) - model.CfGuid = types.StringPointerValue(instance.CfGuid) - model.CfSpaceGuid = types.StringPointerValue(instance.CfSpaceGuid) - model.DashboardUrl = types.StringPointerValue(instance.DashboardUrl) - model.ImageUrl = types.StringPointerValue(instance.ImageUrl) - model.Name = types.StringPointerValue(instance.Name) - model.CfOrganizationGuid = types.StringPointerValue(instance.CfOrganizationGuid) - - if instance.Parameters == nil { - model.Parameters = types.ObjectNull(parametersTypes) - } else { - parameters, err := mapParameters(*instance.Parameters) - if err != nil { - return fmt.Errorf("mapping parameters: %w", err) - } - model.Parameters = parameters - } - return nil -} - -func mapParameters(params map[string]interface{}) (types.Object, error) { - attributes := map[string]attr.Value{} - for attribute := range parametersTypes { - var valueInterface interface{} - var ok bool - - // This replacement is necessary because Terraform does not allow hyphens in attribute names - // And the API uses hyphens in some of the attribute names, which would cause a mismatch - // The following attributes have hyphens in the API but underscores in the schema - hyphenAttributes := []string{ - "down_after_milliseconds", - "failover_timeout", - "lazyfree_lazy_eviction", - "lazyfree_lazy_expire", - "lua_time_limit", - "maxmemory_policy", - "maxmemory_samples", - "notify_keyspace_events", - "tls_ciphers", - "tls_ciphersuites", - "tls_protocols", - } - if slices.Contains(hyphenAttributes, attribute) { - alteredAttribute := strings.ReplaceAll(attribute, "_", "-") - valueInterface, ok = params[alteredAttribute] - } else { - valueInterface, ok = params[attribute] - } - if !ok { - // All fields are optional, so this is ok - // Set the value as nil, will be handled accordingly - valueInterface = nil - } - - var value attr.Value - switch parametersTypes[attribute].(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) - case basetypes.StringType: - if valueInterface == nil { - value = types.StringNull() - } else { - valueString, ok := valueInterface.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) - } - value = types.StringValue(valueString) - } - case basetypes.BoolType: - if valueInterface == nil { - value = types.BoolNull() - } else { - valueBool, ok := valueInterface.(bool) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) - } - value = types.BoolValue(valueBool) - } - case basetypes.Int64Type: - if valueInterface == nil { - value = types.Int64Null() - } else { - // This may be int64, int32, int or float64 - // We try to assert all 4 - var valueInt64 int64 - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) - case int64: - valueInt64 = temp - case int32: - valueInt64 = int64(temp) - case int: - valueInt64 = int64(temp) - case float64: - valueInt64 = int64(temp) - } - value = types.Int64Value(valueInt64) - } - case basetypes.ListType: // Assumed to be a list of strings - if valueInterface == nil { - value = types.ListNull(types.StringType) - } else { - // This may be []string{} or []interface{} - // We try to assert all 2 - var valueList []attr.Value - switch temp := valueInterface.(type) { - default: - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) - case []string: - for _, x := range temp { - valueList = append(valueList, types.StringValue(x)) - } - case []interface{}: - for _, x := range temp { - xString, ok := x.(string) - if !ok { - return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) - } - valueList = append(valueList, types.StringValue(xString)) - } - } - temp2, diags := types.ListValue(types.StringType, valueList) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) - } - value = temp2 - } - } - attributes[attribute] = value - } - - output, diags := types.ObjectValue(parametersTypes, attributes) - if diags.HasError() { - return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) - } - return output, nil -} - -func toCreatePayload(model *Model, parameters *parametersModel) (*redis.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("converting parameters: %w", err) - } - - return &redis.CreateInstancePayload{ - InstanceName: conversion.StringValueToPointer(model.Name), - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toUpdatePayload(model *Model, parameters *parametersModel) (*redis.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - payloadParams, err := toInstanceParams(parameters) - if err != nil { - return nil, fmt.Errorf("converting parameters: %w", err) - } - - return &redis.PartialUpdateInstancePayload{ - Parameters: payloadParams, - PlanId: conversion.StringValueToPointer(model.PlanId), - }, nil -} - -func toInstanceParams(parameters *parametersModel) (*redis.InstanceParameters, error) { - if parameters == nil { - return nil, nil - } - payloadParams := &redis.InstanceParameters{} - - payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) - payloadParams.DownAfterMilliseconds = conversion.Int64ValueToPointer(parameters.DownAfterMilliseconds) - payloadParams.EnableMonitoring = conversion.BoolValueToPointer(parameters.EnableMonitoring) - payloadParams.FailoverTimeout = conversion.Int64ValueToPointer(parameters.FailoverTimeout) - payloadParams.Graphite = conversion.StringValueToPointer(parameters.Graphite) - payloadParams.LazyfreeLazyEviction = redis.InstanceParametersGetLazyfreeLazyEvictionAttributeType(conversion.StringValueToPointer(parameters.LazyfreeLazyEviction)) - payloadParams.LazyfreeLazyExpire = redis.InstanceParametersGetLazyfreeLazyExpireAttributeType(conversion.StringValueToPointer(parameters.LazyfreeLazyExpire)) - payloadParams.LuaTimeLimit = conversion.Int64ValueToPointer(parameters.LuaTimeLimit) - payloadParams.MaxDiskThreshold = conversion.Int64ValueToPointer(parameters.MaxDiskThreshold) - payloadParams.Maxclients = conversion.Int64ValueToPointer(parameters.Maxclients) - payloadParams.MaxmemoryPolicy = redis.InstanceParametersGetMaxmemoryPolicyAttributeType(conversion.StringValueToPointer(parameters.MaxmemoryPolicy)) - payloadParams.MaxmemorySamples = conversion.Int64ValueToPointer(parameters.MaxmemorySamples) - payloadParams.MetricsFrequency = conversion.Int64ValueToPointer(parameters.MetricsFrequency) - payloadParams.MetricsPrefix = conversion.StringValueToPointer(parameters.MetricsPrefix) - payloadParams.MinReplicasMaxLag = conversion.Int64ValueToPointer(parameters.MinReplicasMaxLag) - payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) - payloadParams.NotifyKeyspaceEvents = conversion.StringValueToPointer(parameters.NotifyKeyspaceEvents) - payloadParams.Snapshot = conversion.StringValueToPointer(parameters.Snapshot) - payloadParams.TlsCiphersuites = conversion.StringValueToPointer(parameters.TlsCiphersuites) - payloadParams.TlsProtocols = redis.InstanceParametersGetTlsProtocolsAttributeType(conversion.StringValueToPointer(parameters.TlsProtocols)) - - var err error - payloadParams.Syslog, err = conversion.StringListToPointer(parameters.Syslog) - if err != nil { - return nil, fmt.Errorf("converting syslog: %w", err) - } - - payloadParams.TlsCiphers, err = conversion.StringListToPointer(parameters.TlsCiphers) - if err != nil { - return nil, fmt.Errorf("converting tls_ciphers: %w", err) - } - - return payloadParams, nil -} - -func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { - projectId := model.ProjectId.ValueString() - res, err := r.client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting Redis offerings: %w", err) - } - - version := model.Version.ValueString() - planName := model.PlanName.ValueString() - availableVersions := "" - availablePlanNames := "" - isValidVersion := false - for _, offer := range *res.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) - continue - } - isValidVersion = true - - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue - } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - model.PlanId = types.StringPointerValue(plan.Id) - return nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) - } - } - - if !isValidVersion { - return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) - } - return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) -} - -func loadPlanNameAndVersion(ctx context.Context, client *redis.APIClient, model *Model) error { - projectId := model.ProjectId.ValueString() - planId := model.PlanId.ValueString() - res, err := client.ListOfferings(ctx, projectId).Execute() - if err != nil { - return fmt.Errorf("getting Redis offerings: %w", err) - } - - for _, offer := range *res.Offerings { - for _, plan := range *offer.Plans { - if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { - model.PlanName = types.StringPointerValue(plan.Name) - model.Version = types.StringPointerValue(offer.Version) - return nil - } - } - } - - return fmt.Errorf("couldn't find plan_name and version for plan_id '%s'", planId) -} diff --git a/stackit/internal/services/redis/instance/resource_test.go b/stackit/internal/services/redis/instance/resource_test.go deleted file mode 100644 index 9221878a..00000000 --- a/stackit/internal/services/redis/instance/resource_test.go +++ /dev/null @@ -1,373 +0,0 @@ -package redis - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" -) - -var fixtureModelParameters = types.ObjectValueMust(parametersTypes, map[string]attr.Value{ - "sgw_acl": types.StringValue("acl"), - "down_after_milliseconds": types.Int64Value(10), - "enable_monitoring": types.BoolValue(true), - "failover_timeout": types.Int64Value(10), - "graphite": types.StringValue("1.1.1.1:91"), - "lazyfree_lazy_eviction": types.StringValue(string(redis.INSTANCEPARAMETERSLAZYFREE_LAZY_EVICTION_NO)), - "lazyfree_lazy_expire": types.StringValue(string(redis.INSTANCEPARAMETERSLAZYFREE_LAZY_EXPIRE_NO)), - "lua_time_limit": types.Int64Value(10), - "max_disk_threshold": types.Int64Value(100), - "maxclients": types.Int64Value(10), - "maxmemory_policy": types.StringValue(string(redis.INSTANCEPARAMETERSMAXMEMORY_POLICY_ALLKEYS_LRU)), - "maxmemory_samples": types.Int64Value(10), - "metrics_frequency": types.Int64Value(10), - "metrics_prefix": types.StringValue("prefix"), - "min_replicas_max_lag": types.Int64Value(10), - "monitoring_instance_id": types.StringValue("mid"), - "notify_keyspace_events": types.StringValue("events"), - "snapshot": types.StringValue("snapshot"), - "syslog": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("syslog"), - types.StringValue("syslog2"), - }), - "tls_ciphers": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ciphers1"), - types.StringValue("ciphers2"), - }), - "tls_ciphersuites": types.StringValue("ciphersuites"), - "tls_protocols": types.StringValue(string(redis.INSTANCEPARAMETERSTLS_PROTOCOLS__2)), -}) - -var fixtureInstanceParameters = redis.InstanceParameters{ - SgwAcl: utils.Ptr("acl"), - DownAfterMilliseconds: utils.Ptr(int64(10)), - EnableMonitoring: utils.Ptr(true), - FailoverTimeout: utils.Ptr(int64(10)), - Graphite: utils.Ptr("1.1.1.1:91"), - LazyfreeLazyEviction: redis.INSTANCEPARAMETERSLAZYFREE_LAZY_EVICTION_NO.Ptr(), - LazyfreeLazyExpire: redis.INSTANCEPARAMETERSLAZYFREE_LAZY_EXPIRE_NO.Ptr(), - LuaTimeLimit: utils.Ptr(int64(10)), - MaxDiskThreshold: utils.Ptr(int64(100)), - Maxclients: utils.Ptr(int64(10)), - MaxmemoryPolicy: redis.INSTANCEPARAMETERSMAXMEMORY_POLICY_ALLKEYS_LRU.Ptr(), - MaxmemorySamples: utils.Ptr(int64(10)), - MetricsFrequency: utils.Ptr(int64(10)), - MetricsPrefix: utils.Ptr("prefix"), - MinReplicasMaxLag: utils.Ptr(int64(10)), - MonitoringInstanceId: utils.Ptr("mid"), - NotifyKeyspaceEvents: utils.Ptr("events"), - Snapshot: utils.Ptr("snapshot"), - Syslog: &[]string{"syslog", "syslog2"}, - TlsCiphers: &[]string{"ciphers1", "ciphers2"}, - TlsCiphersuites: utils.Ptr("ciphersuites"), - TlsProtocols: redis.INSTANCEPARAMETERSTLS_PROTOCOLS__2.Ptr(), -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *redis.Instance - expected Model - isValid bool - }{ - { - "default_values", - &redis.Instance{}, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringNull(), - Name: types.StringNull(), - CfGuid: types.StringNull(), - CfSpaceGuid: types.StringNull(), - DashboardUrl: types.StringNull(), - ImageUrl: types.StringNull(), - CfOrganizationGuid: types.StringNull(), - Parameters: types.ObjectNull(parametersTypes), - }, - true, - }, - { - "simple_values", - &redis.Instance{ - PlanId: utils.Ptr("plan"), - CfGuid: utils.Ptr("cf"), - CfSpaceGuid: utils.Ptr("space"), - DashboardUrl: utils.Ptr("dashboard"), - ImageUrl: utils.Ptr("image"), - InstanceId: utils.Ptr("iid"), - Name: utils.Ptr("name"), - CfOrganizationGuid: utils.Ptr("org"), - Parameters: &map[string]interface{}{ - "sgw_acl": "acl", - "down-after-milliseconds": int64(10), - "enable_monitoring": true, - "failover-timeout": int64(10), - "graphite": "1.1.1.1:91", - "lazyfree-lazy-eviction": string(redis.INSTANCEPARAMETERSLAZYFREE_LAZY_EVICTION_NO), - "lazyfree-lazy-expire": string(redis.INSTANCEPARAMETERSLAZYFREE_LAZY_EXPIRE_NO), - "lua-time-limit": int64(10), - "max_disk_threshold": int64(100), - "maxclients": int64(10), - "maxmemory-policy": string(redis.INSTANCEPARAMETERSMAXMEMORY_POLICY_ALLKEYS_LRU), - "maxmemory-samples": int64(10), - "metrics_frequency": int64(10), - "metrics_prefix": "prefix", - "min_replicas_max_lag": int64(10), - "monitoring_instance_id": "mid", - "notify-keyspace-events": "events", - "snapshot": "snapshot", - "syslog": []string{"syslog", "syslog2"}, - "tls-ciphers": []string{"ciphers1", "ciphers2"}, - "tls-ciphersuites": "ciphersuites", - "tls-protocols": string(redis.INSTANCEPARAMETERSTLS_PROTOCOLS__2), - }, - }, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - PlanId: types.StringValue("plan"), - Name: types.StringValue("name"), - CfGuid: types.StringValue("cf"), - CfSpaceGuid: types.StringValue("space"), - DashboardUrl: types.StringValue("dashboard"), - ImageUrl: types.StringValue("image"), - CfOrganizationGuid: types.StringValue("org"), - Parameters: fixtureModelParameters, - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &redis.Instance{}, - Model{}, - false, - }, - { - "wrong_param_types_1", - &redis.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": true, - }, - }, - Model{}, - false, - }, - { - "wrong_param_types_2", - &redis.Instance{ - Parameters: &map[string]interface{}{ - "sgw_acl": 1, - }, - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *redis.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &redis.CreateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &redis.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - PlanId: types.StringValue(""), - Parameters: fixtureModelParameters, - }, - &redis.CreateInstancePayload{ - InstanceName: utils.Ptr(""), - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - Name: types.StringValue("name"), - PlanId: types.StringValue("plan"), - }, - &redis.CreateInstancePayload{ - InstanceName: utils.Ptr("name"), - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toCreatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *redis.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &redis.PartialUpdateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - PlanId: types.StringValue("plan"), - Parameters: fixtureModelParameters, - }, - &redis.PartialUpdateInstancePayload{ - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr("plan"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - PlanId: types.StringValue(""), - Parameters: fixtureModelParameters, - }, - &redis.PartialUpdateInstancePayload{ - Parameters: &fixtureInstanceParameters, - PlanId: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - { - "nil_parameters", - &Model{ - PlanId: types.StringValue("plan"), - }, - &redis.PartialUpdateInstancePayload{ - PlanId: utils.Ptr("plan"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var parameters *parametersModel - if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { - parameters = ¶metersModel{} - diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) - if diags.HasError() { - t.Fatalf("Error converting parameters: %v", diags.Errors()) - } - } - } - output, err := toUpdatePayload(tt.input, parameters) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/redis/redis_acc_test.go b/stackit/internal/services/redis/redis_acc_test.go deleted file mode 100644 index e86fe3ca..00000000 --- a/stackit/internal/services/redis/redis_acc_test.go +++ /dev/null @@ -1,325 +0,0 @@ -package redis_test - -import ( - "context" - "fmt" - "regexp" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// Instance resource data -var instanceResource = map[string]string{ - "project_id": testutil.ProjectId, - "name": testutil.ResourceNameWithDateTime("redis"), - "plan_id": "96e24604-7a43-4ff8-9ba4-609d4235a137", - "plan_name": "stackit-redis-1.4.10-single", - "version": "6", - "sgw_acl_invalid": "1.2.3.4/4", - "sgw_acl_valid": "192.168.0.0/16", - "sgw_acl_valid2": "10.10.10.0/24", -} - -func parametersConfig(params map[string]string) string { - nonStringParams := []string{ - "down_after_milliseconds", - "enable_monitoring", - "failover_timeout", - "lua_time_limit", - "max_disk_threshold", - "maxclients", - "maxmemory_samples", - "metrics_frequency", - "min_replicas_max_lag", - "syslog", - "tls_ciphers", - } - parameters := "parameters = {" - for k, v := range params { - if utils.Contains(nonStringParams, k) { - parameters += fmt.Sprintf("%s = %s\n", k, v) - } else { - parameters += fmt.Sprintf("%s = %q\n", k, v) - } - } - parameters += "\n}" - return parameters -} - -func resourceConfig(params map[string]string) string { - return fmt.Sprintf(` - %s - - resource "stackit_redis_instance" "instance" { - project_id = "%s" - name = "%s" - plan_name = "%s" - version = "%s" - %s - } - - %s - `, - testutil.RedisProviderConfig(), - instanceResource["project_id"], - instanceResource["name"], - instanceResource["plan_name"], - instanceResource["version"], - parametersConfig(params), - resourceConfigCredential(), - ) -} - -func resourceConfigCredential() string { - return ` - resource "stackit_redis_credential" "credential" { - project_id = stackit_redis_instance.instance.project_id - instance_id = stackit_redis_instance.instance.instance_id - } - ` -} - -func TestAccRedisResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckRedisDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_invalid"]}), - ExpectError: regexp.MustCompile(`.*sgw_acl is invalid.*`), - }, - // Creation - { - Config: resourceConfig(map[string]string{ - "sgw_acl": instanceResource["sgw_acl_valid"], - "down_after_milliseconds": "10000", - "enable_monitoring": "false", - "failover_timeout": "30000", - "graphite": "graphite.example.com:2003", - "lazyfree_lazy_eviction": "no", - "lazyfree_lazy_expire": "no", - "lua_time_limit": "5000", - "max_disk_threshold": "80", - "maxclients": "10000", - "maxmemory_policy": "volatile-lru", - "maxmemory_samples": "5", - "metrics_frequency": "10", - "metrics_prefix": "prefix", - "min_replicas_max_lag": "15", - "notify_keyspace_events": "Ex", - "syslog": `["syslog.example.com:123"]`, - "tls_protocols": "TLSv1.2", - }), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_redis_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "plan_name", instanceResource["plan_name"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "name", instanceResource["name"]), - - // Instance Params data - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.down_after_milliseconds", "10000"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.enable_monitoring", "false"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.failover_timeout", "30000"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.graphite", "graphite.example.com:2003"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.lazyfree_lazy_eviction", "no"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.lazyfree_lazy_expire", "no"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.lua_time_limit", "5000"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.max_disk_threshold", "80"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.maxclients", "10000"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.maxmemory_policy", "volatile-lru"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.maxmemory_samples", "5"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.metrics_frequency", "10"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.metrics_prefix", "prefix"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.min_replicas_max_lag", "15"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.notify_keyspace_events", "Ex"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.syslog.#", "1"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.syslog.0", "syslog.example.com:123"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.tls_protocols", "TLSv1.2"), - - // Credential data - resource.TestCheckResourceAttrPair( - "stackit_redis_credential.credential", "project_id", - "stackit_redis_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_redis_credential.credential", "instance_id", - "stackit_redis_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_redis_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("stackit_redis_credential.credential", "host"), - ), - }, - // data source - { - Config: fmt.Sprintf(` - %s - - data "stackit_redis_instance" "instance" { - project_id = stackit_redis_instance.instance.project_id - instance_id = stackit_redis_instance.instance.instance_id - } - - data "stackit_redis_credential" "credential" { - project_id = stackit_redis_credential.credential.project_id - instance_id = stackit_redis_credential.credential.instance_id - credential_id = stackit_redis_credential.credential.credential_id - }`, - resourceConfig(nil), - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_redis_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrPair("stackit_redis_instance.instance", "instance_id", - "data.stackit_redis_credential.credential", "instance_id"), - resource.TestCheckResourceAttrPair("data.stackit_redis_instance.instance", "instance_id", - "data.stackit_redis_credential.credential", "instance_id"), - resource.TestCheckResourceAttr("data.stackit_redis_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("data.stackit_redis_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttrSet("data.stackit_redis_instance.instance", "parameters.sgw_acl"), - - // Credentials data - resource.TestCheckResourceAttr("data.stackit_redis_credential.credential", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("data.stackit_redis_credential.credential", "credential_id"), - resource.TestCheckResourceAttrSet("data.stackit_redis_credential.credential", "host"), - resource.TestCheckResourceAttrSet("data.stackit_redis_credential.credential", "port"), - resource.TestCheckResourceAttrSet("data.stackit_redis_credential.credential", "uri"), - resource.TestCheckResourceAttrSet("data.stackit_redis_credential.credential", "load_balanced_host"), - ), - }, - // Import - { - ResourceName: "stackit_redis_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_redis_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_redis_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "stackit_redis_credential.credential", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_redis_credential.credential"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_redis_credential.credential") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - credentialId, ok := r.Primary.Attributes["credential_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute credential_id") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: resourceConfig(map[string]string{"sgw_acl": instanceResource["sgw_acl_valid2"]}), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "project_id", instanceResource["project_id"]), - resource.TestCheckResourceAttrSet("stackit_redis_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "plan_id", instanceResource["plan_id"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "plan_name", instanceResource["plan_name"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "version", instanceResource["version"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "name", instanceResource["name"]), - resource.TestCheckResourceAttr("stackit_redis_instance.instance", "parameters.sgw_acl", instanceResource["sgw_acl_valid2"]), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func checkInstanceDeleteSuccess(i *redis.Instance) bool { - if *i.LastOperation.Type != redis.INSTANCELASTOPERATIONTYPE_DELETE { - return false - } - - if *i.LastOperation.Type == redis.INSTANCELASTOPERATIONTYPE_DELETE { - if *i.LastOperation.State != redis.INSTANCELASTOPERATIONSTATE_SUCCEEDED { - return false - } else if strings.Contains(*i.LastOperation.Description, "DeleteFailed") || strings.Contains(*i.LastOperation.Description, "failed") { - return false - } - } - return true -} - -func testAccCheckRedisDestroy(s *terraform.State) error { - ctx := context.Background() - var client *redis.APIClient - var err error - if testutil.RedisCustomEndpoint == "" { - client, err = redis.NewAPIClient( - config.WithRegion("eu01"), - ) - } else { - client, err = redis.NewAPIClient( - config.WithEndpoint(testutil.RedisCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_redis_instance" { - continue - } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := *instancesResp.Instances - for i := range instances { - if instances[i].InstanceId == nil { - continue - } - if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { - if !checkInstanceDeleteSuccess(&instances[i]) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) - } - } - } - } - return nil -} diff --git a/stackit/internal/services/redis/utils/util.go b/stackit/internal/services/redis/utils/util.go deleted file mode 100644 index 0b9963ee..00000000 --- a/stackit/internal/services/redis/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *redis.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.RedisCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.RedisCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := redis.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/redis/utils/util_test.go b/stackit/internal/services/redis/utils/util_test.go deleted file mode 100644 index 51918317..00000000 --- a/stackit/internal/services/redis/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://redis-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *redis.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *redis.APIClient { - apiClient, err := redis.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - RedisCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *redis.APIClient { - apiClient, err := redis.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/resourcemanager/folder/datasource.go b/stackit/internal/services/resourcemanager/folder/datasource.go deleted file mode 100644 index 702d76d3..00000000 --- a/stackit/internal/services/resourcemanager/folder/datasource.go +++ /dev/null @@ -1,183 +0,0 @@ -package folder - -import ( - "context" - "fmt" - "net/http" - "regexp" - - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &folderDataSource{} - _ datasource.DataSourceWithConfigure = &folderDataSource{} -) - -// NewFolderDataSource is a helper function to simplify the provider implementation. -func NewFolderDataSource() datasource.DataSource { - return &folderDataSource{} -} - -// folderDataSource is the data source implementation. -type folderDataSource struct { - client *resourcemanager.APIClient -} - -// Metadata returns the data source type name. -func (d *folderDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_resourcemanager_folder" -} - -func (d *folderDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "Resource Manager client configured") -} - -// Schema defines the schema for the data source. -func (d *folderDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Resource Manager folder data source schema. To identify the folder, you need to provide the container_id.", - "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", - "container_id": "Folder container ID. Globally unique, user-friendly identifier.", - "folder_id": "Folder UUID identifier. Globally unique folder identifier", - "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported.", - "name": "The name of the folder.", - "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}.", - "owner_email": "Email address of the owner of the folder. This value is only considered during creation. Changing it afterwards will have no effect.", - "creation_time": "Date-time at which the folder was created.", - "update_time": "Date-time at which the folder was last modified.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "container_id": schema.StringAttribute{ - Description: descriptions["container_id"], - Validators: []validator.String{ - validate.NoSeparator(), - }, - Required: true, - }, - "folder_id": schema.StringAttribute{ - Description: descriptions["folder_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "parent_container_id": schema.StringAttribute{ - Description: descriptions["parent_container_id"], - Computed: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - ElementType: types.StringType, - Computed: true, - Validators: []validator.Map{ - mapvalidator.KeysAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - mapvalidator.ValueStringsAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - }, - }, - "creation_time": schema.StringAttribute{ - Description: descriptions["creation_time"], - Computed: true, - }, - "update_time": schema.StringAttribute{ - Description: descriptions["update_time"], - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *folderDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "container_id", containerId) - - folderResp, err := d.client.GetFolderDetails(ctx, containerId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading folder", - fmt.Sprintf("folder with ID %q does not exist.", containerId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("folder with ID %q not found or forbidden access", containerId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFolderFields(ctx, folderResp, &model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading folder", fmt.Sprintf("Processing API response: %v", err)) - return - } - - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Resource Manager folder read") -} diff --git a/stackit/internal/services/resourcemanager/folder/resource.go b/stackit/internal/services/resourcemanager/folder/resource.go deleted file mode 100644 index 4a6b0b9e..00000000 --- a/stackit/internal/services/resourcemanager/folder/resource.go +++ /dev/null @@ -1,521 +0,0 @@ -package folder - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - "time" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &folderResource{} - _ resource.ResourceWithConfigure = &folderResource{} - _ resource.ResourceWithImportState = &folderResource{} -) - -const ( - projectOwnerRole = "owner" -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - FolderId types.String `tfsdk:"folder_id"` - ContainerId types.String `tfsdk:"container_id"` - ContainerParentId types.String `tfsdk:"parent_container_id"` - Name types.String `tfsdk:"name"` - Labels types.Map `tfsdk:"labels"` - CreationTime types.String `tfsdk:"creation_time"` - UpdateTime types.String `tfsdk:"update_time"` -} - -type ResourceModel struct { - Model - OwnerEmail types.String `tfsdk:"owner_email"` -} - -// NewFolderResource is a helper function to simplify the provider implementation. -func NewFolderResource() resource.Resource { - return &folderResource{} -} - -// folderResource is the resource implementation. -type folderResource struct { - client *resourcemanager.APIClient -} - -// Metadata returns the resource type name. -func (r *folderResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_resourcemanager_folder" -} - -// Configure adds the provider configured client to the resource. -func (r *folderResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Resource Manager client configured") -} - -// Schema defines the schema for the resource. -func (r *folderResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Resource Manager folder resource schema.", - "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", - "container_id": "Folder container ID. Globally unique, user-friendly identifier.", - "folder_id": "Folder UUID identifier. Globally unique folder identifier", - "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported.", - "name": "The name of the folder.", - "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}.", - "owner_email": "Email address of the owner of the folder. This value is only considered during creation. Changing it afterwards will have no effect.", - "creation_time": "Date-time at which the folder was created.", - "update_time": "Date-time at which the folder was last modified.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "container_id": schema.StringAttribute{ - Description: descriptions["container_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "folder_id": schema.StringAttribute{ - Description: descriptions["folder_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "parent_container_id": schema.StringAttribute{ - Description: descriptions["parent_container_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - ElementType: types.StringType, - Optional: true, - Validators: []validator.Map{ - mapvalidator.KeysAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - mapvalidator.ValueStringsAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - }, - }, - "owner_email": schema.StringAttribute{ - Description: descriptions["owner_email"], - Required: true, - }, - "creation_time": schema.StringAttribute{ - Description: descriptions["creation_time"], - Computed: true, - }, - "update_time": schema.StringAttribute{ - Description: descriptions["update_time"], - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *folderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - tflog.Info(ctx, "creating folder") - var model ResourceModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerParentId := model.ContainerParentId.ValueString() - folderName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "container_parent_id", containerParentId) - ctx = tflog.SetField(ctx, "folder_name", folderName) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - folderCreateResp, err := r.client.CreateFolder(ctx).CreateFolderPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if folderCreateResp.ContainerId == nil || *folderCreateResp.ContainerId == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", "Container ID is missing") - return - } - - // This sleep is currently needed due to the IAM Cache. - time.Sleep(10 * time.Second) - - folderGetResponse, err := r.client.GetFolderDetails(ctx, *folderCreateResp.ContainerId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating folder", fmt.Sprintf("Calling API: %v", err)) - return - } - - err = mapFolderFields(ctx, folderGetResponse, &model.Model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "API response processing error", err.Error()) - return - } - - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) - tflog.Info(ctx, "Folder created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *folderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - folderName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "folder_name", folderName) - ctx = tflog.SetField(ctx, "container_id", containerId) - - folderResp, err := r.client.GetFolderDetails(ctx, containerId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusForbidden { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading folder", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFolderFields(ctx, folderResp, &model.Model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading folder", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set refreshed model - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Resource Manager folder read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *folderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model ResourceModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "container_id", containerId) - - // Generate API request body from model - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing folder - _, err = r.client.PartialUpdateFolder(ctx, containerId).PartialUpdateFolderPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Fetch updated folder - folderResp, err := r.client.GetFolderDetails(ctx, containerId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Calling API for updated data: %v", err)) - return - } - - err = mapFolderFields(ctx, folderResp, &model.Model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating folder", fmt.Sprintf("Processing API response: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Resource Manager folder updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *folderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model ResourceModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "container_id", containerId) - - // Delete existing folder - err := r.client.DeleteFolder(ctx, containerId).Execute() - if err != nil { - core.LogAndAddError( - ctx, - &resp.Diagnostics, - "Error deleting folder. Deletion may fail because associated projects remain hidden for up to 7 days after user deletion due to technical requirements.", - fmt.Sprintf("Calling API: %v", err), - ) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Resource Manager folder deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: container_id -func (r *folderResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 1 || idParts[0] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing folder", - fmt.Sprintf("Expected import identifier with format: [container_id] Got: %q", req.ID), - ) - return - } - - ctx = tflog.SetField(ctx, "container_id", req.ID) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("container_id"), req.ID)...) - tflog.Info(ctx, "Resource Manager folder state imported") -} - -// mapFolderFields maps folder fields from a response into the Terraform model and optionally updates state. -func mapFolderFields( - ctx context.Context, - folderGetResponse *resourcemanager.GetFolderDetailsResponse, - model *Model, - state *tfsdk.State, -) error { - if folderGetResponse == nil { - return fmt.Errorf("folder get response is nil") - } - - var folderId string - if model.FolderId.ValueString() != "" { - folderId = model.FolderId.ValueString() - } else if folderGetResponse.FolderId != nil { - folderId = *folderGetResponse.FolderId - } else { - return fmt.Errorf("folder id not present") - } - - var containerId string - if model.ContainerId.ValueString() != "" { - containerId = model.ContainerId.ValueString() - } else if folderGetResponse.ContainerId != nil { - containerId = *folderGetResponse.ContainerId - } else { - return fmt.Errorf("container id not present") - } - - var err error - var tfLabels basetypes.MapValue - if folderGetResponse.Labels != nil && len(*folderGetResponse.Labels) > 0 { - tfLabels, err = conversion.ToTerraformStringMap(ctx, *folderGetResponse.Labels) - if err != nil { - return fmt.Errorf("converting to StringValue map: %w", err) - } - } else { - tfLabels = types.MapNull(types.StringType) - } - - var containerParentIdTF basetypes.StringValue - if folderGetResponse.Parent != nil { - if _, err := uuid.Parse(model.ContainerParentId.ValueString()); err == nil { - // the provided containerParent is the UUID identifier - containerParentIdTF = types.StringPointerValue(folderGetResponse.Parent.Id) - } else { - // the provided containerParent is the user-friendly container id - containerParentIdTF = types.StringPointerValue(folderGetResponse.Parent.ContainerId) - } - } else { - containerParentIdTF = types.StringNull() - } - - model.Id = types.StringValue(containerId) - model.FolderId = types.StringValue(folderId) - model.ContainerId = types.StringValue(containerId) - model.ContainerParentId = containerParentIdTF - model.Name = types.StringPointerValue(folderGetResponse.Name) - model.Labels = tfLabels - model.CreationTime = types.StringValue(folderGetResponse.CreationTime.Format(time.RFC3339)) - model.UpdateTime = types.StringValue(folderGetResponse.UpdateTime.Format(time.RFC3339)) - - if state != nil { - diags := diag.Diagnostics{} - diags.Append(state.SetAttribute(ctx, path.Root("id"), model.Id)...) - diags.Append(state.SetAttribute(ctx, path.Root("folder_id"), model.FolderId)...) - diags.Append(state.SetAttribute(ctx, path.Root("container_id"), model.ContainerId)...) - diags.Append(state.SetAttribute(ctx, path.Root("parent_container_id"), model.ContainerParentId)...) - diags.Append(state.SetAttribute(ctx, path.Root("name"), model.Name)...) - diags.Append(state.SetAttribute(ctx, path.Root("labels"), model.Labels)...) - diags.Append(state.SetAttribute(ctx, path.Root("creation_time"), model.CreationTime)...) - diags.Append(state.SetAttribute(ctx, path.Root("update_time"), model.UpdateTime)...) - if diags.HasError() { - return fmt.Errorf("update terraform state: %w", core.DiagsToError(diags)) - } - } - - return nil -} - -func toMembersPayload(model *ResourceModel) (*[]resourcemanager.Member, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if model.OwnerEmail.IsNull() { - return nil, fmt.Errorf("owner_email is null") - } - - return &[]resourcemanager.Member{ - { - Subject: model.OwnerEmail.ValueStringPointer(), - Role: sdkUtils.Ptr(projectOwnerRole), - }, - }, nil -} - -func toCreatePayload(model *ResourceModel) (*resourcemanager.CreateFolderPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - members, err := toMembersPayload(model) - if err != nil { - return nil, fmt.Errorf("processing members: %w", err) - } - - modelLabels := model.Labels.Elements() - labels, err := conversion.ToOptStringMap(modelLabels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &resourcemanager.CreateFolderPayload{ - ContainerParentId: conversion.StringValueToPointer(model.ContainerParentId), - Labels: labels, - Members: members, - Name: conversion.StringValueToPointer(model.Name), - }, nil -} - -func toUpdatePayload(model *ResourceModel) (*resourcemanager.PartialUpdateFolderPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - modelLabels := model.Labels.Elements() - labels, err := conversion.ToOptStringMap(modelLabels) - if err != nil { - return nil, fmt.Errorf("converting to GO map: %w", err) - } - - return &resourcemanager.PartialUpdateFolderPayload{ - ContainerParentId: conversion.StringValueToPointer(model.ContainerParentId), - Name: conversion.StringValueToPointer(model.Name), - Labels: labels, - }, nil -} diff --git a/stackit/internal/services/resourcemanager/folder/resource_test.go b/stackit/internal/services/resourcemanager/folder/resource_test.go deleted file mode 100644 index 500f66f1..00000000 --- a/stackit/internal/services/resourcemanager/folder/resource_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package folder - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" -) - -func TestMapFolderFields(t *testing.T) { - testUUID := uuid.New().String() - baseTime := time.Now() - createTime := baseTime - updateTime := baseTime.Add(1 * time.Hour) - - tests := []struct { - description string - uuidContainerParentId bool - projectResp *resourcemanager.GetFolderDetailsResponse - expected Model - expectedLabels *map[string]string - isValid bool - }{ - { - description: "default_ok", - uuidContainerParentId: false, - projectResp: &resourcemanager.GetFolderDetailsResponse{ - ContainerId: utils.Ptr("cid"), - FolderId: utils.Ptr("fid"), - CreationTime: &createTime, - UpdateTime: &updateTime, - }, - expected: Model{ - Id: types.StringValue("cid"), - ContainerId: types.StringValue("cid"), - FolderId: types.StringValue("fid"), - ContainerParentId: types.StringNull(), - Name: types.StringNull(), - CreationTime: types.StringValue(createTime.Format(time.RFC3339)), - UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), - }, - expectedLabels: nil, - isValid: true, - }, - { - description: "container_parent_id_ok", - uuidContainerParentId: false, - projectResp: &resourcemanager.GetFolderDetailsResponse{ - ContainerId: utils.Ptr("cid"), - FolderId: utils.Ptr("fid"), - Labels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - Parent: &resourcemanager.Parent{ - ContainerId: utils.Ptr("parent_cid"), - Id: utils.Ptr(testUUID), - }, - Name: utils.Ptr("name"), - CreationTime: &createTime, - UpdateTime: &updateTime, - }, - expected: Model{ - Id: types.StringValue("cid"), - ContainerId: types.StringValue("cid"), - FolderId: types.StringValue("fid"), - ContainerParentId: types.StringValue("parent_cid"), - Name: types.StringValue("name"), - CreationTime: types.StringValue(createTime.Format(time.RFC3339)), - UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), - }, - expectedLabels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - isValid: true, - }, - { - description: "uuid_parent_id_ok", - uuidContainerParentId: true, - projectResp: &resourcemanager.GetFolderDetailsResponse{ - ContainerId: utils.Ptr("cid"), - FolderId: utils.Ptr("fid"), - Labels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - Parent: &resourcemanager.Parent{ - ContainerId: utils.Ptr("parent_cid"), - Id: utils.Ptr(testUUID), // simulate UUID logic - }, - Name: utils.Ptr("name"), - CreationTime: &createTime, - UpdateTime: &updateTime, - }, - expected: Model{ - Id: types.StringValue("cid"), - ContainerId: types.StringValue("cid"), - FolderId: types.StringValue("fid"), - ContainerParentId: types.StringValue(testUUID), - Name: types.StringValue("name"), - CreationTime: types.StringValue(createTime.Format(time.RFC3339)), - UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), - }, - expectedLabels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - isValid: true, - }, - { - description: "response_nil_fail", - uuidContainerParentId: false, - projectResp: nil, - expected: Model{}, - expectedLabels: nil, - isValid: false, - }, - { - description: "no_resource_id", - uuidContainerParentId: false, - projectResp: &resourcemanager.GetFolderDetailsResponse{}, - expected: Model{}, - expectedLabels: nil, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - if tt.expectedLabels == nil { - tt.expected.Labels = types.MapNull(types.StringType) - } else { - convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.expectedLabels) - if err != nil { - t.Fatalf("Error converting to terraform string map: %v", err) - } - tt.expected.Labels = convertedLabels - } - var containerParentId = types.StringNull() - if tt.uuidContainerParentId { - containerParentId = types.StringValue(testUUID) - } else if tt.projectResp != nil && tt.projectResp.Parent != nil && tt.projectResp.Parent.ContainerId != nil { - containerParentId = types.StringValue(*tt.projectResp.Parent.ContainerId) - } - - model := &Model{ - ContainerId: tt.expected.ContainerId, - ContainerParentId: containerParentId, - } - - err := mapFolderFields(context.Background(), tt.projectResp, model, nil) - - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *ResourceModel - inputLabels *map[string]string - expected *resourcemanager.CreateFolderPayload - isValid bool - }{ - { - "mapping_with_conversions", - &ResourceModel{ - Model: Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - }, - OwnerEmail: types.StringValue("john.doe@stackit.cloud"), - }, - &map[string]string{ - "label1": "1", - "label2": "2", - }, - &resourcemanager.CreateFolderPayload{ - ContainerParentId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "1", - "label2": "2", - }, - Members: &[]resourcemanager.Member{ - { - Subject: utils.Ptr("john.doe@stackit.cloud"), - Role: utils.Ptr("owner"), - }, - }, - Name: utils.Ptr("name"), - }, - true, - }, - { - "no owner_email fails", - &ResourceModel{ - Model: Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - }, - }, - &map[string]string{}, - nil, - false, - }, - { - "nil_model", - nil, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - if tt.input != nil { - if tt.inputLabels == nil { - tt.input.Labels = types.MapNull(types.StringType) - } else { - convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.inputLabels) - if err != nil { - t.Fatalf("Error converting to terraform string map: %v", err) - } - tt.input.Labels = convertedLabels - } - } - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *ResourceModel - inputLabels *map[string]string - expected *resourcemanager.PartialUpdateFolderPayload - isValid bool - }{ - { - "default_ok", - &ResourceModel{}, - nil, - &resourcemanager.PartialUpdateFolderPayload{ - ContainerParentId: nil, - Labels: nil, - Name: nil, - }, - true, - }, - { - "mapping_with_conversions_ok", - &ResourceModel{ - Model: Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - }, - OwnerEmail: types.StringValue("owner_email"), - }, - &map[string]string{ - "label1": "1", - "label2": "2", - }, - &resourcemanager.PartialUpdateFolderPayload{ - ContainerParentId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "1", - "label2": "2", - }, - Name: utils.Ptr("name"), - }, - true, - }, - { - "nil_model", - nil, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - if tt.input != nil { - if tt.inputLabels == nil { - tt.input.Labels = types.MapNull(types.StringType) - } else { - convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.inputLabels) - if err != nil { - t.Fatalf("Error converting to terraform string map: %v", err) - } - tt.input.Labels = convertedLabels - } - } - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToMembersPayload(t *testing.T) { - type args struct { - model *ResourceModel - } - tests := []struct { - name string - args args - want *[]resourcemanager.Member - wantErr bool - }{ - { - name: "missing model", - args: args{}, - want: nil, - wantErr: true, - }, - { - name: "empty model", - args: args{ - model: &ResourceModel{}, - }, - want: nil, - wantErr: true, - }, - { - name: "ok", - args: args{ - model: &ResourceModel{ - OwnerEmail: types.StringValue("john.doe@stackit.cloud"), - }, - }, - want: &[]resourcemanager.Member{ - { - Subject: utils.Ptr("john.doe@stackit.cloud"), - Role: utils.Ptr("owner"), - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toMembersPayload(tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toMembersPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("toMembersPayload() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/stackit/internal/services/resourcemanager/project/datasource.go b/stackit/internal/services/resourcemanager/project/datasource.go deleted file mode 100644 index 0f25d34b..00000000 --- a/stackit/internal/services/resourcemanager/project/datasource.go +++ /dev/null @@ -1,200 +0,0 @@ -package project - -import ( - "context" - "fmt" - "net/http" - "regexp" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &projectDataSource{} - _ datasource.DataSourceWithConfigure = &projectDataSource{} -) - -// NewProjectDataSource is a helper function to simplify the provider implementation. -func NewProjectDataSource() datasource.DataSource { - return &projectDataSource{} -} - -// projectDataSource is the data source implementation. -type projectDataSource struct { - client *resourcemanager.APIClient -} - -// Metadata returns the data source type name. -func (d *projectDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_resourcemanager_project" -} - -func (d *projectDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - d.client = apiClient - tflog.Info(ctx, "Resource Manager project client configured") -} - -// Schema defines the schema for the data source. -func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Resource Manager project data source schema. To identify the project, you need to provider either project_id or container_id. If you provide both, project_id will be used.", - "id": "Terraform's internal data source. ID. It is structured as \"`container_id`\".", - "project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.", - "container_id": "Project container ID. Globally unique, user-friendly identifier.", - "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported", - "name": "Project name.", - "labels": `Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`, - "creation_time": "Date-time at which the project was created.", - "update_time": "Date-time at which the project was last modified.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Optional: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "container_id": schema.StringAttribute{ - Description: descriptions["container_id"], - Optional: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "parent_container_id": schema.StringAttribute{ - Description: descriptions["parent_container_id"], - Computed: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - ElementType: types.StringType, - Computed: true, - Validators: []validator.Map{ - mapvalidator.KeysAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - mapvalidator.ValueStringsAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - }, - }, - "creation_time": schema.StringAttribute{ - Description: descriptions["creation_time"], - Computed: true, - }, - "update_time": schema.StringAttribute{ - Description: descriptions["update_time"], - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "container_id", containerId) - - if containerId == "" && projectId == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", "Either container_id or project_id must be set") - return - } - - // set project identifier. If projectId is provided, it takes precedence over containerId - var identifier = containerId - identifierType := "Container" - if projectId != "" { - identifier = projectId - identifierType = "Project" - } - - projectResp, err := d.client.GetProject(ctx, identifier).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading project", - fmt.Sprintf("%s with ID %q does not exist.", identifierType, identifier), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("%s with ID %q not found or forbidden access", identifierType, identifier), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapProjectFields(ctx, projectResp, &model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API response: %v", err)) - return - } - - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Resource Manager project read") -} diff --git a/stackit/internal/services/resourcemanager/project/resource.go b/stackit/internal/services/resourcemanager/project/resource.go deleted file mode 100644 index d0cd06c1..00000000 --- a/stackit/internal/services/resourcemanager/project/resource.go +++ /dev/null @@ -1,521 +0,0 @@ -package project - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strings" - "time" - - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &projectResource{} - _ resource.ResourceWithConfigure = &projectResource{} - _ resource.ResourceWithImportState = &projectResource{} -) - -const ( - projectOwnerRole = "owner" -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - ContainerId types.String `tfsdk:"container_id"` - ContainerParentId types.String `tfsdk:"parent_container_id"` - Name types.String `tfsdk:"name"` - Labels types.Map `tfsdk:"labels"` - CreationTime types.String `tfsdk:"creation_time"` - UpdateTime types.String `tfsdk:"update_time"` -} - -type ResourceModel struct { - Model - OwnerEmail types.String `tfsdk:"owner_email"` -} - -// NewProjectResource is a helper function to simplify the provider implementation. -func NewProjectResource() resource.Resource { - return &projectResource{} -} - -// projectResource is the resource implementation. -type projectResource struct { - client *resourcemanager.APIClient -} - -// Metadata returns the resource type name. -func (r *projectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_resourcemanager_project" -} - -// Configure adds the provider configured client to the resource. -func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Resource Manager project client configured") -} - -// Schema defines the schema for the resource. -func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": fmt.Sprintf("%s\n\n%s", - "Resource Manager project resource schema.", - "-> In case you're getting started with an empty STACKIT organization and want to use this resource to create projects in it, check out [this guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/stackit_org_service_account) for how to create a service account which you can use for authentication in the STACKIT Terraform provider.", - ), - "id": "Terraform's internal resource ID. It is structured as \"`container_id`\".", - "project_id": "Project UUID identifier. This is the ID that can be used in most of the other resources to identify the project.", - "container_id": "Project container ID. Globally unique, user-friendly identifier.", - "parent_container_id": "Parent resource identifier. Both container ID (user-friendly) and UUID are supported", - "name": "Project name.", - "labels": "Labels are key-value string pairs which can be attached to a resource container. A label key must match the regex [A-ZÄÜÖa-zäüöß0-9_-]{1,64}. A label value must match the regex ^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}. \nTo create a project within a STACKIT Network Area, setting the label `networkArea=` is required. This can not be changed after project creation.", - "owner_email": "Email address of the owner of the project. This value is only considered during creation. Changing it afterwards will have no effect.", - "creation_time": "Date-time at which the project was created.", - "update_time": "Date-time at which the project was last modified.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - }, - }, - "container_id": schema.StringAttribute{ - Description: descriptions["container_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "parent_container_id": schema.StringAttribute{ - Description: descriptions["parent_container_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.LengthAtMost(63), - }, - }, - "labels": schema.MapAttribute{ - Description: descriptions["labels"], - ElementType: types.StringType, - Optional: true, - Validators: []validator.Map{ - mapvalidator.KeysAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - mapvalidator.ValueStringsAre( - stringvalidator.RegexMatches( - regexp.MustCompile(`^$|[A-ZÄÜÖa-zäüöß0-9_-]{1,64}`), - "must match expression"), - ), - }, - }, - "owner_email": schema.StringAttribute{ - Description: descriptions["owner_email"], - Required: true, - }, - "creation_time": schema.StringAttribute{ - Description: descriptions["creation_time"], - Computed: true, - }, - "update_time": schema.StringAttribute{ - Description: descriptions["update_time"], - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "project_container_id", containerId) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new project - createResp, err := r.client.CreateProject(ctx).CreateProjectPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - respContainerId := *createResp.ContainerId - - // If the request has not been processed yet and the containerId doesn't exist, - // the waiter will fail with authentication error, so wait some time before checking the creation - waitResp, err := wait.CreateProjectWaitHandler(ctx, r.client, respContainerId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - err = mapProjectFields(ctx, waitResp, &model.Model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Resource Manager project created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model ResourceModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "container_id", containerId) - - projectResp, err := r.client.GetProject(ctx, containerId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusForbidden { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapProjectFields(ctx, projectResp, &model.Model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set refreshed model - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Resource Manager project read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model ResourceModel - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "container_id", containerId) - - // Generate API request body from model - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing project - _, err = r.client.PartialUpdateProject(ctx, containerId).PartialUpdateProjectPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Fetch updated project - projectResp, err := r.client.GetProject(ctx, containerId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Calling API for updated data: %v", err)) - return - } - - err = mapProjectFields(ctx, projectResp, &model.Model, &resp.State) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating project", fmt.Sprintf("Processing API response: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Resource Manager project updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model ResourceModel - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - containerId := model.ContainerId.ValueString() - ctx = tflog.SetField(ctx, "container_id", containerId) - - // Delete existing project - err := r.client.DeleteProject(ctx, containerId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteProjectWaitHandler(ctx, r.client, containerId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting project", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - - tflog.Info(ctx, "Resource Manager project deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: container_id -func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 1 || idParts[0] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing project", - fmt.Sprintf("Expected import identifier with format: [container_id] Got: %q", req.ID), - ) - return - } - - ctx = tflog.SetField(ctx, "container_id", req.ID) - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("container_id"), req.ID)...) - tflog.Info(ctx, "Resource Manager Project state imported") -} - -// mapProjectFields maps the API response to the Terraform model and update the Terraform state -func mapProjectFields(ctx context.Context, projectResp *resourcemanager.GetProjectResponse, model *Model, state *tfsdk.State) (err error) { - if projectResp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var projectId string - if model.ProjectId.ValueString() != "" { - projectId = model.ProjectId.ValueString() - } else if projectResp.ProjectId != nil { - projectId = *projectResp.ProjectId - } else { - return fmt.Errorf("project id not present") - } - - var containerId string - if model.ContainerId.ValueString() != "" { - containerId = model.ContainerId.ValueString() - } else if projectResp.ContainerId != nil { - containerId = *projectResp.ContainerId - } else { - return fmt.Errorf("container id not present") - } - - var labels basetypes.MapValue - if projectResp.Labels != nil && len(*projectResp.Labels) != 0 { - labels, err = conversion.ToTerraformStringMap(ctx, *projectResp.Labels) - if err != nil { - return fmt.Errorf("converting to StringValue map: %w", err) - } - } else { - labels = types.MapNull(types.StringType) - } - - var containerParentIdTF basetypes.StringValue - if projectResp.Parent != nil { - if _, err := uuid.Parse(model.ContainerParentId.ValueString()); err == nil { - // the provided containerParentId is the UUID identifier - containerParentIdTF = types.StringPointerValue(projectResp.Parent.Id) - } else { - // the provided containerParentId is the user-friendly container id - containerParentIdTF = types.StringPointerValue(projectResp.Parent.ContainerId) - } - } else { - containerParentIdTF = types.StringNull() - } - - model.Id = types.StringValue(containerId) - model.ProjectId = types.StringValue(projectId) - model.ContainerParentId = containerParentIdTF - model.ContainerId = types.StringValue(containerId) - model.Name = types.StringPointerValue(projectResp.Name) - model.Labels = labels - model.CreationTime = types.StringValue(projectResp.CreationTime.Format(time.RFC3339)) - model.UpdateTime = types.StringValue(projectResp.UpdateTime.Format(time.RFC3339)) - - if state != nil { - diags := diag.Diagnostics{} - diags.Append(state.SetAttribute(ctx, path.Root("id"), model.Id)...) - diags.Append(state.SetAttribute(ctx, path.Root("project_id"), model.ProjectId)...) - diags.Append(state.SetAttribute(ctx, path.Root("parent_container_id"), model.ContainerParentId)...) - diags.Append(state.SetAttribute(ctx, path.Root("container_id"), model.ContainerId)...) - diags.Append(state.SetAttribute(ctx, path.Root("name"), model.Name)...) - diags.Append(state.SetAttribute(ctx, path.Root("labels"), model.Labels)...) - diags.Append(state.SetAttribute(ctx, path.Root("creation_time"), model.CreationTime)...) - diags.Append(state.SetAttribute(ctx, path.Root("update_time"), model.UpdateTime)...) - if diags.HasError() { - return fmt.Errorf("update terraform state: %w", core.DiagsToError(diags)) - } - } - - return nil -} - -func toMembersPayload(model *ResourceModel) (*[]resourcemanager.Member, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - if model.OwnerEmail.IsNull() { - return nil, fmt.Errorf("owner_email is null") - } - - return &[]resourcemanager.Member{ - { - Subject: model.OwnerEmail.ValueStringPointer(), - Role: sdkUtils.Ptr(projectOwnerRole), - }, - }, nil -} - -func toCreatePayload(model *ResourceModel) (*resourcemanager.CreateProjectPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - members, err := toMembersPayload(model) - if err != nil { - return nil, fmt.Errorf("processing members: %w", err) - } - - modelLabels := model.Labels.Elements() - labels, err := conversion.ToOptStringMap(modelLabels) - if err != nil { - return nil, fmt.Errorf("converting to Go map: %w", err) - } - - return &resourcemanager.CreateProjectPayload{ - ContainerParentId: conversion.StringValueToPointer(model.ContainerParentId), - Labels: labels, - Members: members, - Name: conversion.StringValueToPointer(model.Name), - }, nil -} - -func toUpdatePayload(model *ResourceModel) (*resourcemanager.PartialUpdateProjectPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - modelLabels := model.Labels.Elements() - labels, err := conversion.ToOptStringMap(modelLabels) - if err != nil { - return nil, fmt.Errorf("converting to GO map: %w", err) - } - - return &resourcemanager.PartialUpdateProjectPayload{ - ContainerParentId: conversion.StringValueToPointer(model.ContainerParentId), - Name: conversion.StringValueToPointer(model.Name), - Labels: labels, - }, nil -} diff --git a/stackit/internal/services/resourcemanager/project/resource_test.go b/stackit/internal/services/resourcemanager/project/resource_test.go deleted file mode 100644 index 28aaded6..00000000 --- a/stackit/internal/services/resourcemanager/project/resource_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package project - -import ( - "context" - "reflect" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" -) - -func TestMapProjectFields(t *testing.T) { - testUUID := uuid.New().String() - baseTime := time.Now() - createTime := baseTime - updateTime := baseTime.Add(1 * time.Hour) - - tests := []struct { - description string - uuidContainerParentId bool - projectResp *resourcemanager.GetProjectResponse - expected Model - expectedLabels *map[string]string - isValid bool - }{ - { - description: "default_ok", - uuidContainerParentId: false, - projectResp: &resourcemanager.GetProjectResponse{ - ContainerId: utils.Ptr("cid"), - ProjectId: utils.Ptr("pid"), - CreationTime: &createTime, - UpdateTime: &updateTime, - }, - expected: Model{ - Id: types.StringValue("cid"), - ContainerId: types.StringValue("cid"), - ProjectId: types.StringValue("pid"), - ContainerParentId: types.StringNull(), - Name: types.StringNull(), - CreationTime: types.StringValue(createTime.Format(time.RFC3339)), - UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), - }, - expectedLabels: nil, - isValid: true, - }, - { - description: "container_parent_id_ok", - uuidContainerParentId: false, - projectResp: &resourcemanager.GetProjectResponse{ - ContainerId: utils.Ptr("cid"), - ProjectId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - Parent: &resourcemanager.Parent{ - ContainerId: utils.Ptr("parent_cid"), - Id: utils.Ptr("parent_pid"), - }, - Name: utils.Ptr("name"), - CreationTime: &createTime, - UpdateTime: &updateTime, - }, - expected: Model{ - Id: types.StringValue("cid"), - ContainerId: types.StringValue("cid"), - ProjectId: types.StringValue("pid"), - ContainerParentId: types.StringValue("parent_cid"), - Name: types.StringValue("name"), - CreationTime: types.StringValue(createTime.Format(time.RFC3339)), - UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), - }, - expectedLabels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - isValid: true, - }, - { - description: "uuid_parent_id_ok", - uuidContainerParentId: true, - projectResp: &resourcemanager.GetProjectResponse{ - ContainerId: utils.Ptr("cid"), - ProjectId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - Parent: &resourcemanager.Parent{ - ContainerId: utils.Ptr("parent_cid"), - Id: utils.Ptr(testUUID), // simulate UUID logic - }, - Name: utils.Ptr("name"), - CreationTime: &createTime, - UpdateTime: &updateTime, - }, - expected: Model{ - Id: types.StringValue("cid"), - ContainerId: types.StringValue("cid"), - ProjectId: types.StringValue("pid"), - ContainerParentId: types.StringValue(testUUID), - Name: types.StringValue("name"), - CreationTime: types.StringValue(createTime.Format(time.RFC3339)), - UpdateTime: types.StringValue(updateTime.Format(time.RFC3339)), - }, - expectedLabels: &map[string]string{ - "label1": "ref1", - "label2": "ref2", - }, - isValid: true, - }, - { - description: "response_nil_fail", - uuidContainerParentId: false, - projectResp: nil, - expected: Model{}, - expectedLabels: nil, - isValid: false, - }, - { - description: "no_resource_id", - uuidContainerParentId: false, - projectResp: &resourcemanager.GetProjectResponse{}, - expected: Model{}, - expectedLabels: nil, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - if tt.expectedLabels == nil { - tt.expected.Labels = types.MapNull(types.StringType) - } else { - convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.expectedLabels) - if err != nil { - t.Fatalf("Error converting to terraform string map: %v", err) - } - tt.expected.Labels = convertedLabels - } - var containerParentId = types.StringNull() - if tt.uuidContainerParentId { - containerParentId = types.StringValue(testUUID) - } else if tt.projectResp != nil && tt.projectResp.Parent != nil && tt.projectResp.Parent.ContainerId != nil { - containerParentId = types.StringValue(*tt.projectResp.Parent.ContainerId) - } - - model := &Model{ - ContainerId: tt.expected.ContainerId, - ContainerParentId: containerParentId, - } - - err := mapProjectFields(context.Background(), tt.projectResp, model, nil) - - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(model, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *ResourceModel - inputLabels *map[string]string - expected *resourcemanager.CreateProjectPayload - isValid bool - }{ - { - "mapping_with_conversions", - &ResourceModel{ - Model: Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - }, - OwnerEmail: types.StringValue("john.doe@stackit.cloud"), - }, - &map[string]string{ - "label1": "1", - "label2": "2", - }, - &resourcemanager.CreateProjectPayload{ - ContainerParentId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "1", - "label2": "2", - }, - Members: &[]resourcemanager.Member{ - { - Subject: utils.Ptr("john.doe@stackit.cloud"), - Role: utils.Ptr("owner"), - }, - }, - Name: utils.Ptr("name"), - }, - true, - }, - { - "no owner_email fails", - &ResourceModel{ - Model: Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - }, - }, - &map[string]string{}, - nil, - false, - }, - { - "nil_model", - nil, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - if tt.input != nil { - if tt.inputLabels == nil { - tt.input.Labels = types.MapNull(types.StringType) - } else { - convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.inputLabels) - if err != nil { - t.Fatalf("Error converting to terraform string map: %v", err) - } - tt.input.Labels = convertedLabels - } - } - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *ResourceModel - inputLabels *map[string]string - expected *resourcemanager.PartialUpdateProjectPayload - isValid bool - }{ - { - "default_ok", - &ResourceModel{}, - nil, - &resourcemanager.PartialUpdateProjectPayload{ - ContainerParentId: nil, - Labels: nil, - Name: nil, - }, - true, - }, - { - "mapping_with_conversions_ok", - &ResourceModel{ - Model: Model{ - ContainerParentId: types.StringValue("pid"), - Name: types.StringValue("name"), - }, - OwnerEmail: types.StringValue("owner_email"), - }, - &map[string]string{ - "label1": "1", - "label2": "2", - }, - &resourcemanager.PartialUpdateProjectPayload{ - ContainerParentId: utils.Ptr("pid"), - Labels: &map[string]string{ - "label1": "1", - "label2": "2", - }, - Name: utils.Ptr("name"), - }, - true, - }, - { - "nil_model", - nil, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - if tt.input != nil { - if tt.inputLabels == nil { - tt.input.Labels = types.MapNull(types.StringType) - } else { - convertedLabels, err := conversion.ToTerraformStringMap(context.Background(), *tt.inputLabels) - if err != nil { - t.Fatalf("Error converting to terraform string map: %v", err) - } - tt.input.Labels = convertedLabels - } - } - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToMembersPayload(t *testing.T) { - type args struct { - model *ResourceModel - } - tests := []struct { - name string - args args - want *[]resourcemanager.Member - wantErr bool - }{ - { - name: "missing model", - args: args{}, - want: nil, - wantErr: true, - }, - { - name: "empty model", - args: args{ - model: &ResourceModel{}, - }, - want: nil, - wantErr: true, - }, - { - name: "ok", - args: args{ - model: &ResourceModel{ - OwnerEmail: types.StringValue("john.doe@stackit.cloud"), - }, - }, - want: &[]resourcemanager.Member{ - { - Subject: utils.Ptr("john.doe@stackit.cloud"), - Role: utils.Ptr("owner"), - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := toMembersPayload(tt.args.model) - if (err != nil) != tt.wantErr { - t.Errorf("toMembersPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("toMembersPayload() got = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go deleted file mode 100644 index 600445f3..00000000 --- a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go +++ /dev/null @@ -1,573 +0,0 @@ -package resourcemanager_test - -import ( - "context" - _ "embed" - "errors" - "fmt" - "maps" - "sync" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -//go:embed testdata/resource-project.tf -var resourceProject string - -//go:embed testdata/resource-folder.tf -var resourceFolder string - -var defaultLabels = config.ObjectVariable( - map[string]config.Variable{ - "env": config.StringVariable("prod"), - }, -) - -var projectNameParentContainerId = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var projectNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", projectNameParentContainerId) - -var projectNameParentUUID = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var projectNameParentUUIDUpdated = fmt.Sprintf("%s-updated", projectNameParentUUID) - -var folderNameParentContainerId = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var folderNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", folderNameParentContainerId) - -var folderNameParentUUID = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var folderNameParentUUIDUpdated = fmt.Sprintf("%s-updated", folderNameParentUUID) - -var testConfigResourceProjectParentContainerId = config.Variables{ - "name": config.StringVariable(projectNameParentContainerId), - "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), - "parent_container_id": config.StringVariable(testutil.TestProjectParentContainerID), - "labels": config.ObjectVariable( - map[string]config.Variable{ - "env": config.StringVariable("prod"), - }, - ), -} - -var testConfigResourceProjectParentUUID = config.Variables{ - "name": config.StringVariable(projectNameParentUUID), - "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), - "parent_container_id": config.StringVariable(testutil.TestProjectParentUUID), - "labels": defaultLabels, -} - -var testConfigResourceFolderParentContainerId = config.Variables{ - "name": config.StringVariable(folderNameParentContainerId), - "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), - "parent_container_id": config.StringVariable(testutil.TestProjectParentContainerID), - "labels": defaultLabels, -} - -var testConfigResourceFolderParentUUID = config.Variables{ - "name": config.StringVariable(folderNameParentUUID), - "owner_email": config.StringVariable(testutil.TestProjectServiceAccountEmail), - "parent_container_id": config.StringVariable(testutil.TestProjectParentUUID), - "labels": defaultLabels, -} - -func testConfigProjectNameParentContainerIdUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigResourceProjectParentContainerId)) - maps.Copy(tempConfig, testConfigResourceProjectParentContainerId) - tempConfig["name"] = config.StringVariable(projectNameParentContainerIdUpdated) - return tempConfig -} - -func testConfigProjectNameParentUUIDUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigResourceProjectParentUUID)) - maps.Copy(tempConfig, testConfigResourceProjectParentUUID) - tempConfig["name"] = config.StringVariable(projectNameParentUUIDUpdated) - return tempConfig -} - -func testConfigFolderNameParentContainerIdUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigResourceFolderParentContainerId)) - maps.Copy(tempConfig, testConfigResourceFolderParentContainerId) - tempConfig["name"] = config.StringVariable(folderNameParentContainerIdUpdated) - return tempConfig -} - -func testConfigFolderNameParentUUIDUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigResourceFolderParentUUID)) - maps.Copy(tempConfig, testConfigResourceFolderParentUUID) - tempConfig["name"] = config.StringVariable(folderNameParentUUIDUpdated) - return tempConfig -} - -func TestAccResourceManagerProjectContainerId(t *testing.T) { - t.Logf("TestAccResourceManagerProjectContainerId name: %s", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Create - { - ConfigVariables: testConfigResourceProjectParentContainerId, - Config: testutil.ResourceManagerProviderConfig() + resourceProject, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), - ), - }, - // Data Source - { - ConfigVariables: testConfigResourceProjectParentContainerId, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_resourcemanager_project" "example" { - project_id = stackit_resourcemanager_project.example.project_id - } - `, testutil.ResourceManagerProviderConfig(), resourceProject), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["name"])), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["parent_container_id"])), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "container_id", "stackit_resourcemanager_project.example", "container_id"), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "project_id", "stackit_resourcemanager_project.example", "project_id"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "creation_time"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "update_time"), - ), - }, - // Import - { - ConfigVariables: testConfigResourceProjectParentContainerId, - ResourceName: "stackit_resourcemanager_project.example", - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - return getImportIdFromID(s, "stackit_resourcemanager_project.example", "container_id") - }, - ImportStateVerifyIgnore: []string{"owner_email"}, - }, - // Update - { - ConfigVariables: testConfigProjectNameParentContainerIdUpdated(), - Config: testutil.ResourceManagerProviderConfig() + resourceProject, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigProjectNameParentContainerIdUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentContainerId["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), - ), - }, - }, - }) -} - -func TestAccResourceManagerProjectParentUUID(t *testing.T) { - t.Logf("TestAccResourceManagerProjectParentUUID name: %s", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Create - { - ConfigVariables: testConfigResourceProjectParentUUID, - Config: testutil.ResourceManagerProviderConfig() + resourceProject, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), - ), - }, - // Data Source - { - ConfigVariables: testConfigResourceProjectParentUUID, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_resourcemanager_project" "example" { - project_id = stackit_resourcemanager_project.example.project_id - } - `, testutil.ResourceManagerProviderConfig(), resourceProject), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["name"])), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_project.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "parent_container_id"), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "container_id", "stackit_resourcemanager_project.example", "container_id"), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_project.example", "project_id", "stackit_resourcemanager_project.example", "project_id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "creation_time"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_project.example", "update_time"), - ), - }, - // Import - { - ConfigVariables: testConfigResourceProjectParentUUID, - ResourceName: "stackit_resourcemanager_project.example", - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - return getImportIdFromID(s, "stackit_resourcemanager_project.example", "container_id") - }, - ImportStateVerifyIgnore: []string{"owner_email", "parent_container_id"}, - }, - // Update - { - ConfigVariables: testConfigProjectNameParentUUIDUpdated(), - Config: testutil.ResourceManagerProviderConfig() + resourceProject, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "name", testutil.ConvertConfigVariable(testConfigProjectNameParentUUIDUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceProjectParentUUID["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_project.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "project_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "owner_email"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_project.example", "update_time"), - ), - }, - }, - }) -} - -func TestAccResourceManagerFolderContainerId(t *testing.T) { - t.Logf("TestAccResourceManagerFolderContainerId name: %s", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Create - { - ConfigVariables: testConfigResourceFolderParentContainerId, - Config: testutil.ResourceManagerProviderConfig() + resourceFolder, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), - ), - }, - // Data Source - { - ConfigVariables: testConfigResourceFolderParentContainerId, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_resourcemanager_folder" "example" { - container_id = stackit_resourcemanager_folder.example.container_id - } - `, testutil.ResourceManagerProviderConfig(), resourceFolder), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.env", "prod"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["parent_container_id"])), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "container_id", "stackit_resourcemanager_folder.example", "container_id"), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "project_id", "stackit_resourcemanager_folder.example", "project_id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "creation_time"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "update_time"), - ), - }, - // Import - { - ConfigVariables: testConfigResourceFolderParentContainerId, - ResourceName: "stackit_resourcemanager_folder.example", - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - return getImportIdFromID(s, "stackit_resourcemanager_folder.example", "container_id") - }, - ImportStateVerifyIgnore: []string{"owner_email"}, - }, - // Update - { - ConfigVariables: testConfigFolderNameParentContainerIdUpdated(), - Config: testutil.ResourceManagerProviderConfig() + resourceFolder, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigFolderNameParentContainerIdUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigFolderNameParentContainerIdUpdated()["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigFolderNameParentContainerIdUpdated()["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "owner_email"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), - ), - }, - }, - }) -} - -func TestAccResourceManagerFolderParentUUID(t *testing.T) { - t.Logf("TestAccResourceManagerFolderParentUUID name: %s", testutil.ConvertConfigVariable(testConfigResourceFolderParentContainerId["name"])) - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckDestroy, - Steps: []resource.TestStep{ - // Create - { - ConfigVariables: testConfigResourceFolderParentUUID, - Config: testutil.ResourceManagerProviderConfig() + resourceFolder, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), - ), - }, - // Data Source - { - ConfigVariables: testConfigResourceFolderParentUUID, - Config: fmt.Sprintf(` - %s - %s - - data "stackit_resourcemanager_folder" "example" { - container_id = stackit_resourcemanager_folder.example.container_id - } - `, testutil.ResourceManagerProviderConfig(), resourceFolder), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigResourceFolderParentUUID["name"])), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.%", "1"), - resource.TestCheckResourceAttr("data.stackit_resourcemanager_folder.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "parent_container_id"), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "container_id", "stackit_resourcemanager_folder.example", "container_id"), - resource.TestCheckResourceAttrPair("data.stackit_resourcemanager_folder.example", "project_id", "stackit_resourcemanager_folder.example", "project_id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "id"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "creation_time"), - resource.TestCheckResourceAttrSet("data.stackit_resourcemanager_folder.example", "update_time"), - ), - }, - // Import - { - ConfigVariables: testConfigResourceFolderParentUUID, - ResourceName: "stackit_resourcemanager_folder.example", - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - return getImportIdFromID(s, "stackit_resourcemanager_folder.example", "container_id") - }, - ImportStateVerifyIgnore: []string{"owner_email", "parent_container_id"}, - }, - // Update - { - ConfigVariables: testConfigFolderNameParentUUIDUpdated(), - Config: testutil.ResourceManagerProviderConfig() + resourceFolder, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "name", testutil.ConvertConfigVariable(testConfigFolderNameParentUUIDUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "parent_container_id", testutil.ConvertConfigVariable(testConfigFolderNameParentUUIDUpdated()["parent_container_id"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "owner_email", testutil.ConvertConfigVariable(testConfigFolderNameParentUUIDUpdated()["owner_email"])), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.%", "1"), - resource.TestCheckResourceAttr("stackit_resourcemanager_folder.example", "labels.env", "prod"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "container_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "folder_id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "id"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "owner_email"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "creation_time"), - resource.TestCheckResourceAttrSet("stackit_resourcemanager_folder.example", "update_time"), - ), - }, - }, - }) -} - -func testAccCheckDestroy(s *terraform.State) error { - checkFunctions := []func(s *terraform.State) error{ - testAccCheckResourceManagerProjectsDestroy, - testAccCheckResourceManagerFoldersDestroy, - } - var errs []error - - wg := sync.WaitGroup{} - wg.Add(len(checkFunctions)) - - for _, f := range checkFunctions { - go func() { - err := f(s) - if err != nil { - errs = append(errs, err) - } - wg.Done() - }() - } - wg.Wait() - return errors.Join(errs...) -} - -func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { - ctx := context.Background() - var client *resourcemanager.APIClient - var err error - if testutil.ResourceManagerCustomEndpoint == "" { - client, err = resourcemanager.NewAPIClient() - } else { - client, err = resourcemanager.NewAPIClient( - sdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - projectsToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_resourcemanager_project" { - continue - } - // project terraform ID: "[container_id]" - containerId := rs.Primary.ID - projectsToDestroy = append(projectsToDestroy, containerId) - } - - var containerParentId string - switch { - case testutil.TestProjectParentContainerID != "": - containerParentId = testutil.TestProjectParentContainerID - case testutil.TestProjectParentUUID != "": - containerParentId = testutil.TestProjectParentUUID - default: - return fmt.Errorf("either TestProjectParentContainerID or TestProjectParentUUID must be set") - } - - projectsResp, err := client.ListProjects(ctx).ContainerParentId(containerParentId).Execute() - if err != nil { - return fmt.Errorf("getting projectsResp: %w", err) - } - - items := *projectsResp.Items - for i := range items { - if *items[i].LifecycleState == resourcemanager.LIFECYCLESTATE_DELETING { - continue - } - if !utils.Contains(projectsToDestroy, *items[i].ContainerId) { - continue - } - - err := client.DeleteProjectExecute(ctx, *items[i].ContainerId) - if err != nil { - return fmt.Errorf("destroying project %s during CheckDestroy: %w", *items[i].ContainerId, err) - } - _, err = wait.DeleteProjectWaitHandler(ctx, client, *items[i].ContainerId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying project %s during CheckDestroy: waiting for deletion %w", *items[i].ContainerId, err) - } - } - return nil -} - -func testAccCheckResourceManagerFoldersDestroy(s *terraform.State) error { - ctx := context.Background() - var client *resourcemanager.APIClient - var err error - if testutil.ResourceManagerCustomEndpoint == "" { - client, err = resourcemanager.NewAPIClient() - } else { - client, err = resourcemanager.NewAPIClient( - sdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - foldersToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_resourcemanager_folder" { - continue - } - // project terraform ID: "[container_id]" - containerId := rs.Primary.ID - foldersToDestroy = append(foldersToDestroy, containerId) - } - - var containerParentId string - switch { - case testutil.TestProjectParentContainerID != "": - containerParentId = testutil.TestProjectParentContainerID - case testutil.TestProjectParentUUID != "": - containerParentId = testutil.TestProjectParentUUID - default: - return fmt.Errorf("either TestProjectParentContainerID or TestProjectParentUUID must be set") - } - - projectsResp, err := client.ListFolders(ctx).ContainerParentId(containerParentId).Execute() - if err != nil { - return fmt.Errorf("getting projectsResp: %w", err) - } - - items := *projectsResp.Items - for i := range items { - if !utils.Contains(foldersToDestroy, *items[i].ContainerId) { - continue - } - - err := client.DeleteFolder(ctx, *items[i].ContainerId).Execute() - if err != nil { - return fmt.Errorf("destroying folder %s during CheckDestroy: %w", *items[i].ContainerId, err) - } - } - return nil -} - -func getImportIdFromID(s *terraform.State, resourceName, keyName string) (string, error) { - r, ok := s.RootModule().Resources[resourceName] - if !ok { - return "", fmt.Errorf("couldn't find resource %s", resourceName) - } - id, ok := r.Primary.Attributes[keyName] - if !ok { - return "", fmt.Errorf("couldn't find attribute %s", keyName) - } - return id, nil -} diff --git a/stackit/internal/services/resourcemanager/testdata/resource-folder.tf b/stackit/internal/services/resourcemanager/testdata/resource-folder.tf deleted file mode 100644 index 68f9b0d0..00000000 --- a/stackit/internal/services/resourcemanager/testdata/resource-folder.tf +++ /dev/null @@ -1,12 +0,0 @@ - -variable "parent_container_id" {} -variable "name" {} -variable "labels" {} -variable "owner_email" {} - -resource "stackit_resourcemanager_folder" "example" { - parent_container_id = var.parent_container_id - name = var.name - labels = var.labels - owner_email = var.owner_email -} diff --git a/stackit/internal/services/resourcemanager/testdata/resource-project.tf b/stackit/internal/services/resourcemanager/testdata/resource-project.tf deleted file mode 100644 index 0de2ee19..00000000 --- a/stackit/internal/services/resourcemanager/testdata/resource-project.tf +++ /dev/null @@ -1,12 +0,0 @@ - -variable "parent_container_id" {} -variable "name" {} -variable "labels" {} -variable "owner_email" {} - -resource "stackit_resourcemanager_project" "example" { - parent_container_id = var.parent_container_id - name = var.name - labels = var.labels - owner_email = var.owner_email -} diff --git a/stackit/internal/services/resourcemanager/utils/util.go b/stackit/internal/services/resourcemanager/utils/util.go deleted file mode 100644 index 13454394..00000000 --- a/stackit/internal/services/resourcemanager/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *resourcemanager.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ResourceManagerCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ResourceManagerCustomEndpoint)) - } - apiClient, err := resourcemanager.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/resourcemanager/utils/util_test.go b/stackit/internal/services/resourcemanager/utils/util_test.go deleted file mode 100644 index 352c8fb9..00000000 --- a/stackit/internal/services/resourcemanager/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://resourcemanager-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *resourcemanager.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *resourcemanager.APIClient { - apiClient, err := resourcemanager.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ResourceManagerCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *resourcemanager.APIClient { - apiClient, err := resourcemanager.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/scf/organization/datasource.go b/stackit/internal/services/scf/organization/datasource.go deleted file mode 100644 index 6d8b32ad..00000000 --- a/stackit/internal/services/scf/organization/datasource.go +++ /dev/null @@ -1,180 +0,0 @@ -package organization - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &scfOrganizationDataSource{} - _ datasource.DataSourceWithConfigure = &scfOrganizationDataSource{} -) - -// NewScfOrganizationDataSource creates a new instance of the scfOrganizationDataSource. -func NewScfOrganizationDataSource() datasource.DataSource { - return &scfOrganizationDataSource{} -} - -// scfOrganizationDataSource is the datasource implementation. -type scfOrganizationDataSource struct { - client *scf.APIClient - providerData core.ProviderData -} - -func (s *scfOrganizationDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { - var ok bool - s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - s.client = apiClient - tflog.Info(ctx, "scf client configured") -} - -func (s *scfOrganizationDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform - response.TypeName = request.ProviderTypeName + "_scf_organization" -} - -func (s *scfOrganizationDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform - response.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: descriptions["created_at"], - Computed: true, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - }, - }, - "platform_id": schema.StringAttribute{ - Description: descriptions["platform_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "org_id": schema.StringAttribute{ - Description: descriptions["org_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "quota_id": schema.StringAttribute{ - Description: descriptions["quota_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "region": schema.StringAttribute{ - Description: descriptions["region"], - Optional: true, - Computed: true, - }, - "status": schema.StringAttribute{ - Description: descriptions["status"], - Computed: true, - }, - "suspended": schema.BoolAttribute{ - Description: descriptions["suspended"], - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: descriptions["updated_at"], - Computed: true, - }, - }, - Description: "STACKIT Cloud Foundry organization datasource schema. Must have a `region` specified in the provider configuration.", - } -} - -func (s *scfOrganizationDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model Model - diags := request.Config.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID and instance id of the model - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - - // Extract the region - region := s.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_id", orgId) - ctx = tflog.SetField(ctx, "region", region) - - // Read the current scf organization via orgId - scfOrgResponse, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute() - if err != nil { - utils.LogError( - ctx, - &response.Diagnostics, - err, - "Reading scf organization", - fmt.Sprintf("Organization with ID %q does not exist in project %q.", orgId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", orgId), - }, - ) - response.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(scfOrgResponse, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set the updated state. - diags = response.State.Set(ctx, &model) - response.Diagnostics.Append(diags...) - tflog.Info(ctx, fmt.Sprintf("read scf organization %s", orgId)) -} diff --git a/stackit/internal/services/scf/organization/resource.go b/stackit/internal/services/scf/organization/resource.go deleted file mode 100644 index 4ae7f9b9..00000000 --- a/stackit/internal/services/scf/organization/resource.go +++ /dev/null @@ -1,558 +0,0 @@ -package organization - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/stackitcloud/stackit-sdk-go/services/scf/wait" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &scfOrganizationResource{} - _ resource.ResourceWithConfigure = &scfOrganizationResource{} - _ resource.ResourceWithImportState = &scfOrganizationResource{} - _ resource.ResourceWithModifyPlan = &scfOrganizationResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // Required by Terraform - CreateAt types.String `tfsdk:"created_at"` - Name types.String `tfsdk:"name"` - PlatformId types.String `tfsdk:"platform_id"` - ProjectId types.String `tfsdk:"project_id"` - QuotaId types.String `tfsdk:"quota_id"` - OrgId types.String `tfsdk:"org_id"` - Region types.String `tfsdk:"region"` - Status types.String `tfsdk:"status"` - Suspended types.Bool `tfsdk:"suspended"` - UpdatedAt types.String `tfsdk:"updated_at"` -} - -// NewScfOrganizationResource is a helper function to create a new scf organization resource. -func NewScfOrganizationResource() resource.Resource { - return &scfOrganizationResource{} -} - -// scfOrganizationResource implements the resource interface for scf organization. -type scfOrganizationResource struct { - client *scf.APIClient - providerData core.ProviderData -} - -// descriptions for the attributes in the Schema -var descriptions = map[string]string{ - "id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`org_id`\".", - "created_at": "The time when the organization was created", - "name": "The name of the organization", - "platform_id": "The ID of the platform associated with the organization", - "project_id": "The ID of the project associated with the organization", - "quota_id": "The ID of the quota associated with the organization", - "region": "The resource region. If not defined, the provider region is used", - "status": "The status of the organization (e.g., deleting, delete_failed)", - "suspended": "A boolean indicating whether the organization is suspended", - "org_id": "The ID of the Cloud Foundry Organization", - "updated_at": "The time when the organization was last updated", -} - -func (s *scfOrganizationResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { - var ok bool - s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - s.client = apiClient - tflog.Info(ctx, "scf client configured") -} - -func (s *scfOrganizationResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { - response.TypeName = request.ProviderTypeName + "_scf_organization" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *scfOrganizationResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (s *scfOrganizationResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { - response.Schema = schema.Schema{ - Description: "STACKIT Cloud Foundry organization resource schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "created_at": schema.StringAttribute{ - Description: descriptions["created_at"], - Computed: true, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - }, - }, - "platform_id": schema.StringAttribute{ - Description: descriptions["platform_id"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "org_id": schema.StringAttribute{ - Description: descriptions["org_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "quota_id": schema.StringAttribute{ - Description: descriptions["quota_id"], - Optional: true, - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "region": schema.StringAttribute{ - Description: descriptions["region"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "status": schema.StringAttribute{ - Description: descriptions["status"], - Computed: true, - }, - "suspended": schema.BoolAttribute{ - Description: descriptions["suspended"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.UseStateForUnknown(), - }, - }, - "updated_at": schema.StringAttribute{ - Description: descriptions["updated_at"], - Computed: true, - }, - }, - } -} - -func (s *scfOrganizationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the planned values for the resource. - var model Model - diags := request.Plan.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Set logging context with the project ID and instance ID. - region := model.Region.ValueString() - projectId := model.ProjectId.ValueString() - orgName := model.Name.ValueString() - quotaId := model.QuotaId.ValueString() - suspended := model.Suspended.ValueBool() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_name", orgName) - ctx = tflog.SetField(ctx, "region", region) - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Creating API payload: %v\n", err)) - return - } - - // Create the new scf organization via the API client. - scfOrgCreateResponse, err := s.client.CreateOrganization(ctx, projectId, region). - CreateOrganizationPayload(payload). - Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to create org: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - orgId := *scfOrgCreateResponse.Guid - - // Apply the org quota if provided - if quotaId != "" { - applyOrgQuota, err := s.client.ApplyOrganizationQuota(ctx, projectId, region, orgId).ApplyOrganizationQuotaPayload( - scf.ApplyOrganizationQuotaPayload{ - QuotaId: "aId, - }).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to apply quota: %v", err)) - return - } - model.QuotaId = types.StringPointerValue(applyOrgQuota.QuotaId) - } - - if suspended { - _, err := s.client.UpdateOrganization(ctx, projectId, region, orgId).UpdateOrganizationPayload( - - scf.UpdateOrganizationPayload{ - Suspended: &suspended, - }).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to update suspended: %v", err)) - return - } - } - - // Load the newly created scf organization - scfOrgResponse, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to load created org: %v", err)) - return - } - - err = mapFields(scfOrgResponse, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set the state with fully populated data. - diags = response.State.Set(ctx, model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Scf organization created") -} - -// Read refreshes the Terraform state with the latest scf organization data. -func (s *scfOrganizationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model Model - diags := request.State.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID and instance id of the model - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - // Extract the region - region := s.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_id", orgId) - ctx = tflog.SetField(ctx, "region", region) - // Read the current scf organization via guid - scfOrgResponse, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - response.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(scfOrgResponse, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set the updated state. - diags = response.State.Set(ctx, &model) - response.Diagnostics.Append(diags...) - tflog.Info(ctx, fmt.Sprintf("read scf organization %s", orgId)) -} - -// Update attempts to update the resource. -func (s *scfOrganizationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := request.Plan.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - region := model.Region.ValueString() - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - name := model.Name.ValueString() - quotaId := model.QuotaId.ValueString() - suspended := model.Suspended.ValueBool() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_id", orgId) - ctx = tflog.SetField(ctx, "region", region) - - org, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error retrieving organization state", fmt.Sprintf("Getting organization state: %v", err)) - return - } - - // handle a change of the organization name or the suspended flag - if name != org.GetName() || suspended != org.GetSuspended() { - updatedOrg, err := s.client.UpdateOrganization(ctx, projectId, region, orgId).UpdateOrganizationPayload( - scf.UpdateOrganizationPayload{ - Name: &name, - Suspended: &suspended, - }).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error updating organization", fmt.Sprintf("Processing API payload: %v", err)) - return - } - org = updatedOrg - - ctx = core.LogResponse(ctx) - } - - // handle a quota change of the org - if quotaId != org.GetQuotaId() { - applyOrgQuota, err := s.client.ApplyOrganizationQuota(ctx, projectId, region, orgId).ApplyOrganizationQuotaPayload( - scf.ApplyOrganizationQuotaPayload{ - QuotaId: "aId, - }).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error applying organization quota", fmt.Sprintf("Processing API payload: %v", err)) - return - } - org.QuotaId = applyOrgQuota.QuotaId - } - - err = mapFields(org, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error updating organization", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = response.State.Set(ctx, model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "organization updated") -} - -// Delete deletes the git instance and removes it from the Terraform state on success. -func (s *scfOrganizationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve current state of the resource. - var model Model - diags := request.State.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - - // Extract the region - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_id", orgId) - ctx = tflog.SetField(ctx, "region", region) - - // Call API to delete the existing scf organization. - _, err := s.client.DeleteOrganization(ctx, projectId, region, orgId).Execute() - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting scf organization", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteOrganizationWaitHandler(ctx, s.client, projectId, model.Region.ValueString(), orgId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error waiting for scf org deletion", fmt.Sprintf("SCFOrganization deleting waiting: %v", err)) - return - } - - tflog.Info(ctx, "Scf organization deleted") -} - -func (s *scfOrganizationResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { - // Split the import identifier to extract project ID and email. - idParts := strings.Split(request.ID, core.Separator) - - // Ensure the import identifier format is correct. - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &response.Diagnostics, - "Error importing scf organization", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[org_id] Got: %q", request.ID), - ) - return - } - - projectId := idParts[0] - region := idParts[1] - orgId := idParts[2] - // Set the project id and organization id in the state - response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...) - response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...) - tflog.Info(ctx, "Scf organization state imported") -} - -// mapFields maps a SCF Organization response to the model. -func mapFields(response *scf.Organization, model *Model) error { - if response == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var orgId string - if response.Guid != nil { - orgId = *response.Guid - } else if model.OrgId.ValueString() != "" { - orgId = model.OrgId.ValueString() - } else { - return fmt.Errorf("org id is not present") - } - - var projectId string - if response.ProjectId != nil { - projectId = *response.ProjectId - } else if model.ProjectId.ValueString() != "" { - projectId = model.ProjectId.ValueString() - } else { - return fmt.Errorf("project id is not present") - } - - var region string - if response.Region != nil { - region = *response.Region - } else if model.Region.ValueString() != "" { - region = model.Region.ValueString() - } else { - return fmt.Errorf("region is not present") - } - - // Build the ID by combining the project ID and organization id and assign the model's fields. - model.Id = utils.BuildInternalTerraformId(projectId, region, orgId) - model.ProjectId = types.StringValue(projectId) - model.Region = types.StringValue(region) - model.PlatformId = types.StringPointerValue(response.PlatformId) - model.OrgId = types.StringValue(orgId) - model.Name = types.StringPointerValue(response.Name) - model.Status = types.StringPointerValue(response.Status) - model.Suspended = types.BoolPointerValue(response.Suspended) - model.QuotaId = types.StringPointerValue(response.QuotaId) - model.CreateAt = types.StringValue(response.CreatedAt.String()) - model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) - return nil -} - -// toCreatePayload creates the payload to create a scf organization instance -func toCreatePayload(model *Model) (scf.CreateOrganizationPayload, error) { - if model == nil { - return scf.CreateOrganizationPayload{}, fmt.Errorf("nil model") - } - - payload := scf.CreateOrganizationPayload{ - Name: model.Name.ValueStringPointer(), - } - if !model.PlatformId.IsNull() && !model.PlatformId.IsUnknown() { - payload.PlatformId = model.PlatformId.ValueStringPointer() - } - return payload, nil -} diff --git a/stackit/internal/services/scf/organization/resource_test.go b/stackit/internal/services/scf/organization/resource_test.go deleted file mode 100644 index 956933ff..00000000 --- a/stackit/internal/services/scf/organization/resource_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package organization - -import ( - "fmt" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/scf" -) - -var ( - testOrgId = uuid.New().String() - testProjectId = uuid.New().String() - testPlatformId = uuid.New().String() - testQuotaId = uuid.New().String() - testRegion = "eu01" -) - -func TestMapFields(t *testing.T) { - createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC") - if err != nil { - t.Fatalf("failed to parse test time: %v", err) - } - - tests := []struct { - description string - input *scf.Organization - expected *Model - isValid bool - }{ - { - description: "minimal_input", - input: &scf.Organization{ - Guid: utils.Ptr(testOrgId), - Name: utils.Ptr("scf-org-min-instance"), - Region: utils.Ptr(testRegion), - CreatedAt: &createdTime, - UpdatedAt: &createdTime, - ProjectId: utils.Ptr(testProjectId), - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testOrgId)), - ProjectId: types.StringValue(testProjectId), - Region: types.StringValue(testRegion), - Name: types.StringValue("scf-org-min-instance"), - PlatformId: types.StringNull(), - OrgId: types.StringValue(testOrgId), - QuotaId: types.StringNull(), - Status: types.StringNull(), - Suspended: types.BoolNull(), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - }, - isValid: true, - }, - { - description: "max_input", - input: &scf.Organization{ - CreatedAt: &createdTime, - Guid: utils.Ptr(testOrgId), - Name: utils.Ptr("scf-full-org"), - PlatformId: utils.Ptr(testPlatformId), - ProjectId: utils.Ptr(testProjectId), - QuotaId: utils.Ptr(testQuotaId), - Region: utils.Ptr(testRegion), - Status: nil, - Suspended: utils.Ptr(true), - UpdatedAt: &createdTime, - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testOrgId)), - ProjectId: types.StringValue(testProjectId), - OrgId: types.StringValue(testOrgId), - Name: types.StringValue("scf-full-org"), - Region: types.StringValue(testRegion), - PlatformId: types.StringValue(testPlatformId), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - QuotaId: types.StringValue(testQuotaId), - Status: types.StringNull(), - Suspended: types.BoolValue(true), - }, - isValid: true, - }, - { - description: "nil_org", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "empty_org", - input: &scf.Organization{}, - expected: nil, - isValid: false, - }, - { - description: "missing_id", - input: &scf.Organization{ - Name: utils.Ptr("scf-missing-id"), - }, - expected: nil, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{} - if tt.expected != nil { - state.ProjectId = tt.expected.ProjectId - } - err := mapFields(tt.input, state) - - if tt.isValid && err != nil { - t.Fatalf("expected success, got error: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("expected error, got nil") - } - if tt.isValid { - if diff := cmp.Diff(tt.expected, state); diff != "" { - t.Errorf("unexpected diff (-want +got):\n%s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected scf.CreateOrganizationPayload - expectError bool - }{ - { - description: "default values", - input: &Model{ - Name: types.StringValue("example-org"), - PlatformId: types.StringValue(testPlatformId), - }, - expected: scf.CreateOrganizationPayload{ - Name: utils.Ptr("example-org"), - PlatformId: utils.Ptr(testPlatformId), - }, - expectError: false, - }, - { - description: "nil input model", - input: nil, - expected: scf.CreateOrganizationPayload{}, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - - if tt.expectError && err == nil { - t.Fatalf("expected diagnostics error but got none") - } - - if !tt.expectError && err != nil { - t.Fatalf("unexpected diagnostics error: %v", err) - } - - if diff := cmp.Diff(tt.expected, output); diff != "" { - t.Fatalf("unexpected payload (-want +got):\n%s", diff) - } - }) - } -} diff --git a/stackit/internal/services/scf/organizationmanager/datasource.go b/stackit/internal/services/scf/organizationmanager/datasource.go deleted file mode 100644 index 2f0a1926..00000000 --- a/stackit/internal/services/scf/organizationmanager/datasource.go +++ /dev/null @@ -1,242 +0,0 @@ -package organizationmanager - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &scfOrganizationManagerDataSource{} - _ datasource.DataSourceWithConfigure = &scfOrganizationManagerDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // Required by Terraform - Region types.String `tfsdk:"region"` - PlatformId types.String `tfsdk:"platform_id"` - ProjectId types.String `tfsdk:"project_id"` - OrgId types.String `tfsdk:"org_id"` - UserId types.String `tfsdk:"user_id"` - UserName types.String `tfsdk:"username"` - CreateAt types.String `tfsdk:"created_at"` - UpdatedAt types.String `tfsdk:"updated_at"` -} - -// NewScfOrganizationManagerDataSource creates a new instance of the scfOrganizationDataSource. -func NewScfOrganizationManagerDataSource() datasource.DataSource { - return &scfOrganizationManagerDataSource{} -} - -// scfOrganizationManagerDataSource is the datasource implementation. -type scfOrganizationManagerDataSource struct { - client *scf.APIClient - providerData core.ProviderData -} - -func (s *scfOrganizationManagerDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { - var ok bool - s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - s.client = apiClient - tflog.Info(ctx, "scf client configured for scfOrganizationManagerDataSource") -} - -func (s *scfOrganizationManagerDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform - response.TypeName = request.ProviderTypeName + "_scf_organization_manager" -} - -func (s *scfOrganizationManagerDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform - response.Schema = schema.Schema{ - Description: "STACKIT Cloud Foundry organization manager datasource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "region": schema.StringAttribute{ - Description: descriptions["region"], - Optional: true, - Computed: true, - }, - "platform_id": schema.StringAttribute{ - Description: descriptions["platform_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "org_id": schema.StringAttribute{ - Description: descriptions["org_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - }, - }, - "created_at": schema.StringAttribute{ - Description: descriptions["created_at"], - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: descriptions["updated_at"], - Computed: true, - }, - }, - } -} - -func (s *scfOrganizationManagerDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model DataSourceModel - diags := request.Config.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID and instance id of the model - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - - region := s.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_id", orgId) - ctx = tflog.SetField(ctx, "region", region) - // Read the current scf organization manager via orgId - ScfOrgManager, err := s.client.GetOrgManagerExecute(ctx, projectId, region, orgId) - if err != nil { - utils.LogError( - ctx, - &response.Diagnostics, - err, - "Reading scf organization manager", - fmt.Sprintf("Organization with ID %q does not exist in project %q.", orgId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", orgId), - }, - ) - response.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFieldsDataSource(ScfOrgManager, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization manager", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set the updated state. - diags = response.State.Set(ctx, &model) - response.Diagnostics.Append(diags...) - tflog.Info(ctx, fmt.Sprintf("read scf organization manager %s", orgId)) -} - -func mapFieldsDataSource(response *scf.OrgManager, model *DataSourceModel) error { - if response == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var projectId string - if response.ProjectId != nil { - projectId = *response.ProjectId - } else if model.ProjectId.ValueString() != "" { - projectId = model.ProjectId.ValueString() - } else { - return fmt.Errorf("project id is not present") - } - - var region string - if response.Region != nil { - region = *response.Region - } else if model.Region.ValueString() != "" { - region = model.Region.ValueString() - } else { - return fmt.Errorf("region is not present") - } - - var orgId string - if response.OrgId != nil { - orgId = *response.OrgId - } else if model.OrgId.ValueString() != "" { - orgId = model.OrgId.ValueString() - } else { - return fmt.Errorf("org id is not present") - } - - var userId string - if response.Guid != nil { - userId = *response.Guid - if model.UserId.ValueString() != "" && userId != model.UserId.ValueString() { - return fmt.Errorf("user id mismatch in response and model") - } - } else if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else { - return fmt.Errorf("user id is not present") - } - - model.Id = utils.BuildInternalTerraformId(projectId, region, orgId, userId) - model.Region = types.StringValue(region) - model.PlatformId = types.StringPointerValue(response.PlatformId) - model.ProjectId = types.StringValue(projectId) - model.OrgId = types.StringValue(orgId) - model.UserId = types.StringValue(userId) - model.UserName = types.StringPointerValue(response.Username) - model.CreateAt = types.StringValue(response.CreatedAt.String()) - model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) - return nil -} diff --git a/stackit/internal/services/scf/organizationmanager/datasource_test.go b/stackit/internal/services/scf/organizationmanager/datasource_test.go deleted file mode 100644 index 4ed5e004..00000000 --- a/stackit/internal/services/scf/organizationmanager/datasource_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package organizationmanager - -import ( - "fmt" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/scf" -) - -func TestMapFieldsDataSource(t *testing.T) { - createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC") - if err != nil { - t.Fatalf("failed to parse test time: %v", err) - } - - tests := []struct { - description string - input *scf.OrgManager - expected *DataSourceModel - isValid bool - }{ - { - description: "minimal_input", - input: &scf.OrgManager{ - Guid: utils.Ptr(testUserId), - OrgId: utils.Ptr(testOrgId), - ProjectId: utils.Ptr(testProjectId), - Region: utils.Ptr(testRegion), - CreatedAt: &createdTime, - UpdatedAt: &createdTime, - }, - expected: &DataSourceModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)), - UserId: types.StringValue(testUserId), - OrgId: types.StringValue(testOrgId), - ProjectId: types.StringValue(testProjectId), - Region: types.StringValue(testRegion), - UserName: types.StringNull(), - PlatformId: types.StringNull(), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - }, - isValid: true, - }, - { - description: "max_input", - input: &scf.OrgManager{ - Guid: utils.Ptr(testUserId), - OrgId: utils.Ptr(testOrgId), - ProjectId: utils.Ptr(testProjectId), - PlatformId: utils.Ptr(testPlatformId), - Region: utils.Ptr(testRegion), - CreatedAt: &createdTime, - UpdatedAt: &createdTime, - Username: utils.Ptr("test-user"), - }, - expected: &DataSourceModel{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)), - UserId: types.StringValue(testUserId), - OrgId: types.StringValue(testOrgId), - ProjectId: types.StringValue(testProjectId), - PlatformId: types.StringValue(testPlatformId), - Region: types.StringValue(testRegion), - UserName: types.StringValue("test-user"), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - }, - isValid: true, - }, - { - description: "nil_org", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "empty_org", - input: &scf.OrgManager{}, - expected: nil, - isValid: false, - }, - { - description: "missing_id", - input: &scf.OrgManager{ - Username: utils.Ptr("scf-missing-id"), - }, - expected: nil, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &DataSourceModel{} - if tt.expected != nil { - state.ProjectId = tt.expected.ProjectId - } - err := mapFieldsDataSource(tt.input, state) - - if tt.isValid && err != nil { - t.Fatalf("expected success, got error: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("expected error, got nil") - } - if tt.isValid { - if diff := cmp.Diff(tt.expected, state); diff != "" { - t.Errorf("unexpected diff (-want +got):\n%s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/scf/organizationmanager/resource.go b/stackit/internal/services/scf/organizationmanager/resource.go deleted file mode 100644 index 027fe6c9..00000000 --- a/stackit/internal/services/scf/organizationmanager/resource.go +++ /dev/null @@ -1,484 +0,0 @@ -package organizationmanager - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &scfOrganizationManagerResource{} - _ resource.ResourceWithConfigure = &scfOrganizationManagerResource{} - _ resource.ResourceWithImportState = &scfOrganizationManagerResource{} - _ resource.ResourceWithModifyPlan = &scfOrganizationManagerResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // Required by Terraform - Region types.String `tfsdk:"region"` - PlatformId types.String `tfsdk:"platform_id"` - ProjectId types.String `tfsdk:"project_id"` - OrgId types.String `tfsdk:"org_id"` - UserId types.String `tfsdk:"user_id"` - UserName types.String `tfsdk:"username"` - Password types.String `tfsdk:"password"` - CreateAt types.String `tfsdk:"created_at"` - UpdatedAt types.String `tfsdk:"updated_at"` -} - -// NewScfOrganizationManagerResource is a helper function to create a new scf organization manager resource. -func NewScfOrganizationManagerResource() resource.Resource { - return &scfOrganizationManagerResource{} -} - -// scfOrganizationManagerResource implements the resource interface for scf organization manager. -type scfOrganizationManagerResource struct { - client *scf.APIClient - providerData core.ProviderData -} - -// descriptions for the attributes in the Schema -var descriptions = map[string]string{ - "id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`org_id`,`user_id`\".", - "region": "The region where the organization of the organization manager is located. If not defined, the provider region is used", - "platform_id": "The ID of the platform associated with the organization of the organization manager", - "project_id": "The ID of the project associated with the organization of the organization manager", - "org_id": "The ID of the Cloud Foundry Organization", - "user_id": "The ID of the organization manager user", - "username": "An auto-generated organization manager user name", - "password": "An auto-generated password", - "created_at": "The time when the organization manager was created", - "updated_at": "The time when the organization manager was last updated", -} - -func (s *scfOrganizationManagerResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { // nolint:gocritic // function signature required by Terraform - var ok bool - s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - s.client = apiClient - tflog.Info(ctx, "scf client configured") -} - -func (s *scfOrganizationManagerResource) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform - response.TypeName = request.ProviderTypeName + "_scf_organization_manager" -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *scfOrganizationManagerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -func (s *scfOrganizationManagerResource) Schema(_ context.Context, _ resource.SchemaRequest, response *resource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform - response.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "region": schema.StringAttribute{ - Description: descriptions["region"], - Computed: true, - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "platform_id": schema.StringAttribute{ - Description: descriptions["platform_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "org_id": schema.StringAttribute{ - Description: descriptions["org_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), - }, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Computed: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Computed: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - }, - }, - "password": schema.StringAttribute{ - Description: descriptions["password"], - Computed: true, - Sensitive: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - }, - }, - "created_at": schema.StringAttribute{ - Description: descriptions["created_at"], - Computed: true, - }, - "updated_at": schema.StringAttribute{ - Description: descriptions["updated_at"], - Computed: true, - }, - }, - Description: "STACKIT Cloud Foundry organization manager resource schema.", - } -} - -func (s *scfOrganizationManagerResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the planned values for the resource. - var model Model - diags := request.Plan.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Set logging context with the project ID and username. - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - userName := model.UserName.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "username", userName) - ctx = tflog.SetField(ctx, "region", region) - - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - // Create the new scf organization manager via the API client. - scfOrgManagerCreateResponse, err := s.client.CreateOrgManagerExecute(ctx, projectId, region, orgId) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", fmt.Sprintf("Calling API to create org manager: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFieldsCreate(scfOrgManagerCreateResponse, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization manager", fmt.Sprintf("Mapping fields: %v", err)) - return - } - - // Set the state with fully populated data. - diags = response.State.Set(ctx, model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Scf organization manager created") -} - -func (s *scfOrganizationManagerResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model Model - diags := request.State.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID, region and org id of the model - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - region := s.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_id", orgId) - ctx = tflog.SetField(ctx, "region", region) - - // Read the current scf organization manager via orgId - scfOrgManager, err := s.client.GetOrgManagerExecute(ctx, projectId, region, orgId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusNotFound { - core.LogAndAddWarning(ctx, &response.Diagnostics, "SCF Organization manager not found", "SCF Organization manager not found, remove from state") - response.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization manager", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFieldsRead(scfOrgManager, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization manager", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set the updated state. - diags = response.State.Set(ctx, &model) - response.Diagnostics.Append(diags...) - tflog.Info(ctx, fmt.Sprintf("read scf organization manager %s", orgId)) -} - -func (s *scfOrganizationManagerResource) Update(ctx context.Context, _ resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // organization manager cannot be updated, so we log an error. - core.LogAndAddError(ctx, &response.Diagnostics, "Error updating organization manager", "Organization Manager can't be updated") -} - -func (s *scfOrganizationManagerResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve current state of the resource. - var model Model - diags := request.State.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - orgId := model.OrgId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "org_id", orgId) - ctx = tflog.SetField(ctx, "region", region) - - // Call API to delete the existing scf organization manager. - _, err := s.client.DeleteOrgManagerExecute(ctx, projectId, region, orgId) - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - if ok && oapiErr.StatusCode == http.StatusGone { - tflog.Info(ctx, "Scf organization manager was already deleted") - return - } - core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting scf organization manager", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Scf organization manager deleted") -} - -func (s *scfOrganizationManagerResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { // nolint:gocritic // function signature required by Terraform - // Split the import identifier to extract project ID, region org ID and user ID. - idParts := strings.Split(request.ID, core.Separator) - - // Ensure the import identifier format is correct. - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &response.Diagnostics, - "Error importing scf organization manager", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[org_id],[user_id] Got: %q", request.ID), - ) - return - } - - projectId := idParts[0] - region := idParts[1] - orgId := idParts[2] - userId := idParts[3] - // Set the project id, region organization id and user id in the state - response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("region"), region)...) - response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("org_id"), orgId)...) - response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("user_id"), userId)...) - tflog.Info(ctx, "Scf organization manager state imported") -} - -func mapFieldsCreate(response *scf.OrgManagerResponse, model *Model) error { - if response == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var projectId string - if response.ProjectId != nil { - projectId = *response.ProjectId - } else if model.ProjectId.ValueString() != "" { - projectId = model.ProjectId.ValueString() - } else { - return fmt.Errorf("project id is not present") - } - - var region string - if response.Region != nil { - region = *response.Region - } else if model.Region.ValueString() != "" { - region = model.Region.ValueString() - } else { - return fmt.Errorf("region is not present") - } - - var orgId string - if response.OrgId != nil { - orgId = *response.OrgId - } else if model.OrgId.ValueString() != "" { - orgId = model.OrgId.ValueString() - } else { - return fmt.Errorf("org id is not present") - } - - var userId string - if response.Guid != nil { - userId = *response.Guid - } else if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else { - return fmt.Errorf("user id is not present") - } - - model.Id = utils.BuildInternalTerraformId(projectId, region, orgId, userId) - model.Region = types.StringValue(region) - model.PlatformId = types.StringPointerValue(response.PlatformId) - model.ProjectId = types.StringValue(projectId) - model.OrgId = types.StringValue(orgId) - model.UserId = types.StringValue(userId) - model.UserName = types.StringPointerValue(response.Username) - model.Password = types.StringPointerValue(response.Password) - model.CreateAt = types.StringValue(response.CreatedAt.String()) - model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) - return nil -} - -func mapFieldsRead(response *scf.OrgManager, model *Model) error { - if response == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var projectId string - if response.ProjectId != nil { - projectId = *response.ProjectId - } else if model.ProjectId.ValueString() != "" { - projectId = model.ProjectId.ValueString() - } else { - return fmt.Errorf("project id is not present") - } - - var region string - if response.Region != nil { - region = *response.Region - } else if model.Region.ValueString() != "" { - region = model.Region.ValueString() - } else { - return fmt.Errorf("region is not present") - } - - var orgId string - if response.OrgId != nil { - orgId = *response.OrgId - } else if model.OrgId.ValueString() != "" { - orgId = model.OrgId.ValueString() - } else { - return fmt.Errorf("org id is not present") - } - - var userId string - if response.Guid != nil { - userId = *response.Guid - if model.UserId.ValueString() != "" && userId != model.UserId.ValueString() { - return fmt.Errorf("user id mismatch in response and model") - } - } else if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else { - return fmt.Errorf("user id is not present") - } - - model.Id = utils.BuildInternalTerraformId(projectId, region, orgId, userId) - model.Region = types.StringValue(region) - model.PlatformId = types.StringPointerValue(response.PlatformId) - model.ProjectId = types.StringValue(projectId) - model.OrgId = types.StringValue(orgId) - model.UserId = types.StringValue(userId) - model.UserName = types.StringPointerValue(response.Username) - model.CreateAt = types.StringValue(response.CreatedAt.String()) - model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) - return nil -} diff --git a/stackit/internal/services/scf/organizationmanager/resource_test.go b/stackit/internal/services/scf/organizationmanager/resource_test.go deleted file mode 100644 index 1ca25759..00000000 --- a/stackit/internal/services/scf/organizationmanager/resource_test.go +++ /dev/null @@ -1,233 +0,0 @@ -package organizationmanager - -import ( - "fmt" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/scf" -) - -var ( - testOrgId = uuid.New().String() - testProjectId = uuid.New().String() - testPlatformId = uuid.New().String() - testUserId = uuid.New().String() - testRegion = "eu01" -) - -func TestMapFields(t *testing.T) { - createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC") - if err != nil { - t.Fatalf("failed to parse test time: %v", err) - } - - tests := []struct { - description string - input *scf.OrgManager - expected *Model - isValid bool - }{ - { - description: "minimal_input", - input: &scf.OrgManager{ - Guid: utils.Ptr(testUserId), - OrgId: utils.Ptr(testOrgId), - ProjectId: utils.Ptr(testProjectId), - Region: utils.Ptr(testRegion), - CreatedAt: &createdTime, - UpdatedAt: &createdTime, - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)), - UserId: types.StringValue(testUserId), - OrgId: types.StringValue(testOrgId), - ProjectId: types.StringValue(testProjectId), - Region: types.StringValue(testRegion), - UserName: types.StringNull(), - PlatformId: types.StringNull(), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - }, - isValid: true, - }, - { - description: "max_input", - input: &scf.OrgManager{ - Guid: utils.Ptr(testUserId), - OrgId: utils.Ptr(testOrgId), - ProjectId: utils.Ptr(testProjectId), - PlatformId: utils.Ptr(testPlatformId), - Region: utils.Ptr(testRegion), - CreatedAt: &createdTime, - UpdatedAt: &createdTime, - Username: utils.Ptr("test-user"), - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)), - UserId: types.StringValue(testUserId), - OrgId: types.StringValue(testOrgId), - ProjectId: types.StringValue(testProjectId), - PlatformId: types.StringValue(testPlatformId), - Region: types.StringValue(testRegion), - Password: types.StringNull(), - UserName: types.StringValue("test-user"), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - }, - isValid: true, - }, - { - description: "nil_org", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "empty_org", - input: &scf.OrgManager{}, - expected: nil, - isValid: false, - }, - { - description: "missing_id", - input: &scf.OrgManager{ - Username: utils.Ptr("scf-missing-id"), - }, - expected: nil, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{} - if tt.expected != nil { - state.ProjectId = tt.expected.ProjectId - } - err := mapFieldsRead(tt.input, state) - - if tt.isValid && err != nil { - t.Fatalf("expected success, got error: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("expected error, got nil") - } - if tt.isValid { - if diff := cmp.Diff(tt.expected, state); diff != "" { - t.Errorf("unexpected diff (-want +got):\n%s", diff) - } - } - }) - } -} - -func TestMapFieldsCreate(t *testing.T) { - createdTime, err := time.Parse("2006-01-02 15:04:05 -0700 MST", "2025-01-01 00:00:00 +0000 UTC") - if err != nil { - t.Fatalf("failed to parse test time: %v", err) - } - - tests := []struct { - description string - input *scf.OrgManagerResponse - expected *Model - isValid bool - }{ - { - description: "minimal_input", - input: &scf.OrgManagerResponse{ - Guid: utils.Ptr(testUserId), - OrgId: utils.Ptr(testOrgId), - ProjectId: utils.Ptr(testProjectId), - Region: utils.Ptr(testRegion), - CreatedAt: &createdTime, - UpdatedAt: &createdTime, - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)), - UserId: types.StringValue(testUserId), - OrgId: types.StringValue(testOrgId), - ProjectId: types.StringValue(testProjectId), - Region: types.StringValue(testRegion), - UserName: types.StringNull(), - PlatformId: types.StringNull(), - Password: types.StringNull(), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - }, - isValid: true, - }, - { - description: "max_input", - input: &scf.OrgManagerResponse{ - Guid: utils.Ptr(testUserId), - OrgId: utils.Ptr(testOrgId), - ProjectId: utils.Ptr(testProjectId), - PlatformId: utils.Ptr(testPlatformId), - Region: utils.Ptr(testRegion), - CreatedAt: &createdTime, - UpdatedAt: &createdTime, - Username: utils.Ptr("test-user"), - Password: utils.Ptr("test-password"), - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testProjectId, testRegion, testOrgId, testUserId)), - UserId: types.StringValue(testUserId), - OrgId: types.StringValue(testOrgId), - ProjectId: types.StringValue(testProjectId), - PlatformId: types.StringValue(testPlatformId), - Region: types.StringValue(testRegion), - UserName: types.StringValue("test-user"), - Password: types.StringValue("test-password"), - CreateAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - UpdatedAt: types.StringValue("2025-01-01 00:00:00 +0000 UTC"), - }, - isValid: true, - }, - { - description: "nil_org", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "empty_org", - input: &scf.OrgManagerResponse{}, - expected: nil, - isValid: false, - }, - { - description: "missing_id", - input: &scf.OrgManagerResponse{ - Username: utils.Ptr("scf-missing-id"), - }, - expected: nil, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{} - if tt.expected != nil { - state.ProjectId = tt.expected.ProjectId - } - err := mapFieldsCreate(tt.input, state) - - if tt.isValid && err != nil { - t.Fatalf("expected success, got error: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("expected error, got nil") - } - if tt.isValid { - if diff := cmp.Diff(tt.expected, state); diff != "" { - t.Errorf("unexpected diff (-want +got):\n%s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/scf/platform/datasource.go b/stackit/internal/services/scf/platform/datasource.go deleted file mode 100644 index 5deb1b62..00000000 --- a/stackit/internal/services/scf/platform/datasource.go +++ /dev/null @@ -1,223 +0,0 @@ -package platform - -import ( - "context" - "fmt" - "net/http" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &scfPlatformDataSource{} - _ datasource.DataSourceWithConfigure = &scfPlatformDataSource{} -) - -// NewScfPlatformDataSource creates a new instance of the ScfPlatformDataSource. -func NewScfPlatformDataSource() datasource.DataSource { - return &scfPlatformDataSource{} -} - -// scfPlatformDataSource is the datasource implementation. -type scfPlatformDataSource struct { - client *scf.APIClient - providerData core.ProviderData -} - -func (s *scfPlatformDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { - var ok bool - s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) - if !ok { - return - } - - apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) - if response.Diagnostics.HasError() { - return - } - s.client = apiClient - tflog.Info(ctx, "scf client configured for platform") -} - -func (s *scfPlatformDataSource) Metadata(_ context.Context, request datasource.MetadataRequest, response *datasource.MetadataResponse) { // nolint:gocritic // function signature required by Terraform - response.TypeName = request.ProviderTypeName + "_scf_platform" -} - -type Model struct { - Id types.String `tfsdk:"id"` // Required by Terraform - PlatformId types.String `tfsdk:"platform_id"` - ProjectId types.String `tfsdk:"project_id"` - SystemId types.String `tfsdk:"system_id"` - DisplayName types.String `tfsdk:"display_name"` - Region types.String `tfsdk:"region"` - ApiUrl types.String `tfsdk:"api_url"` - ConsoleUrl types.String `tfsdk:"console_url"` -} - -// descriptions for the attributes in the Schema -var descriptions = map[string]string{ - "id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`platform_id`\".", - "platform_id": "The unique id of the platform", - "project_id": "The ID of the project associated with the platform", - "system_id": "The ID of the platform System", - "display_name": "The name of the platform", - "region": "The region where the platform is located. If not defined, the provider region is used", - "api_url": "The CF API Url of the platform", - "console_url": "The Stratos URL of the platform", -} - -func (s *scfPlatformDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, response *datasource.SchemaResponse) { // nolint:gocritic // function signature required by Terraform - response.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "platform_id": schema.StringAttribute{ - Description: descriptions["platform_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "system_id": schema.StringAttribute{ - Description: descriptions["system_id"], - Computed: true, - }, - "display_name": schema.StringAttribute{ - Description: descriptions["display_name"], - Computed: true, - }, - "region": schema.StringAttribute{ - Description: descriptions["region"], - Optional: true, - Computed: true, - }, - "api_url": schema.StringAttribute{ - Description: descriptions["api_url"], - Computed: true, - }, - "console_url": schema.StringAttribute{ - Description: descriptions["console_url"], - Computed: true, - }, - }, - Description: "STACKIT Cloud Foundry Platform datasource schema.", - } -} - -func (s *scfPlatformDataSource) Read(ctx context.Context, request datasource.ReadRequest, response *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model Model - diags := request.Config.Get(ctx, &model) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID region and platform id of the model - projectId := model.ProjectId.ValueString() - platformId := model.PlatformId.ValueString() - region := s.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "platform_id", platformId) - ctx = tflog.SetField(ctx, "region", region) - - // Read the scf platform - scfPlatformResponse, err := s.client.GetPlatformExecute(ctx, projectId, region, platformId) - if err != nil { - utils.LogError( - ctx, - &response.Diagnostics, - err, - "Reading scf platform", - fmt.Sprintf("Platform with ID %q does not exist in project %q.", platformId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Platform with ID %q not found or forbidden access", platformId), - }, - ) - response.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(scfPlatformResponse, &model) - if err != nil { - core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf platform", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // Set the updated state. - diags = response.State.Set(ctx, &model) - response.Diagnostics.Append(diags...) - tflog.Info(ctx, fmt.Sprintf("read scf Platform %s", platformId)) -} - -// mapFields maps a SCF Platform response to the model. -func mapFields(response *scf.Platforms, model *Model) error { - if response == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var projectId string - if model.ProjectId.ValueString() == "" { - return fmt.Errorf("project id is not present") - } - projectId = model.ProjectId.ValueString() - - var region string - if response.Region != nil { - region = *response.Region - } else if model.Region.ValueString() != "" { - region = model.Region.ValueString() - } else { - return fmt.Errorf("region is not present") - } - - var platformId string - if response.Guid != nil { - platformId = *response.Guid - } else if model.PlatformId.ValueString() != "" { - platformId = model.PlatformId.ValueString() - } else { - return fmt.Errorf("platform id is not present") - } - - // Build the ID - model.Id = utils.BuildInternalTerraformId(projectId, region, platformId) - model.PlatformId = types.StringValue(platformId) - model.ProjectId = types.StringValue(projectId) - model.SystemId = types.StringPointerValue(response.SystemId) - model.DisplayName = types.StringPointerValue(response.DisplayName) - model.Region = types.StringValue(region) - model.ApiUrl = types.StringPointerValue(response.ApiUrl) - model.ConsoleUrl = types.StringPointerValue(response.ConsoleUrl) - return nil -} diff --git a/stackit/internal/services/scf/platform/datasource_test.go b/stackit/internal/services/scf/platform/datasource_test.go deleted file mode 100644 index b15ee231..00000000 --- a/stackit/internal/services/scf/platform/datasource_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package platform - -import ( - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/scf" -) - -var ( - testProjectId = uuid.New().String() - testPlatformId = uuid.New().String() - testRegion = "eu01" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *scf.Platforms - expected *Model - isValid bool - }{ - { - description: "minimal_input", - input: &scf.Platforms{ - Guid: utils.Ptr(testPlatformId), - Region: utils.Ptr(testRegion), - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testPlatformId)), - PlatformId: types.StringValue(testPlatformId), - ProjectId: types.StringValue(testProjectId), - Region: types.StringValue(testRegion), - SystemId: types.StringNull(), - DisplayName: types.StringNull(), - ApiUrl: types.StringNull(), - ConsoleUrl: types.StringNull(), - }, - isValid: true, - }, - { - description: "max_input", - input: &scf.Platforms{ - Guid: utils.Ptr(testPlatformId), - SystemId: utils.Ptr("eu01.01"), - DisplayName: utils.Ptr("scf-full-org"), - Region: utils.Ptr(testRegion), - ApiUrl: utils.Ptr("https://example.scf.stackit.cloud"), - ConsoleUrl: utils.Ptr("https://example.console.scf.stackit.cloud"), - }, - expected: &Model{ - Id: types.StringValue(fmt.Sprintf("%s,%s,%s", testProjectId, testRegion, testPlatformId)), - ProjectId: types.StringValue(testProjectId), - PlatformId: types.StringValue(testPlatformId), - Region: types.StringValue(testRegion), - SystemId: types.StringValue("eu01.01"), - DisplayName: types.StringValue("scf-full-org"), - ApiUrl: types.StringValue("https://example.scf.stackit.cloud"), - ConsoleUrl: types.StringValue("https://example.console.scf.stackit.cloud"), - }, - isValid: true, - }, - { - description: "nil_org", - input: nil, - expected: nil, - isValid: false, - }, - { - description: "empty_org", - input: &scf.Platforms{}, - expected: nil, - isValid: false, - }, - { - description: "missing_id", - input: &scf.Platforms{ - DisplayName: utils.Ptr("scf-missing-id"), - }, - expected: nil, - isValid: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{} - if tt.expected != nil { - state.ProjectId = tt.expected.ProjectId - } - err := mapFields(tt.input, state) - - if tt.isValid && err != nil { - t.Fatalf("expected success, got error: %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("expected error, got nil") - } - if tt.isValid { - if diff := cmp.Diff(tt.expected, state); diff != "" { - t.Errorf("unexpected diff (-want +got):\n%s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/scf/scf_acc_test.go b/stackit/internal/services/scf/scf_acc_test.go deleted file mode 100644 index 3003ba6d..00000000 --- a/stackit/internal/services/scf/scf_acc_test.go +++ /dev/null @@ -1,456 +0,0 @@ -package scf - -import ( - "context" - _ "embed" - "fmt" - "maps" - "strings" - "testing" - - "github.com/stackitcloud/stackit-sdk-go/services/scf" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -//go:embed testdata/resource-min.tf -var resourceMin string - -//go:embed testdata/resource-max.tf -var resourceMax string - -var randName = acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) -var nameMin = fmt.Sprintf("scf-min-%s-org", randName) -var nameMinUpdated = fmt.Sprintf("scf-min-%s-upd-org", randName) -var nameMax = fmt.Sprintf("scf-max-%s-org", randName) -var nameMaxUpdated = fmt.Sprintf("scf-max-%s-upd-org", randName) - -const ( - platformName = "Shared Cloud Foundry (public)" - platformSystemId = "01.cf.eu01" - platformIdMax = "0a3d1188-353a-4004-832c-53039c0e3868" - platformApiUrl = "https://api.system.01.cf.eu01.stackit.cloud" - platformConsoleUrl = "https://console.apps.01.cf.eu01.stackit.cloud" - quotaIdMax = "e22cfe1a-0318-473f-88db-61d62dc629c0" // small - quotaIdMaxUpdated = "5ea6b9ab-4048-4bd9-8a8a-5dd7fc40745d" // medium - suspendedMax = true - region = "eu01" -) - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(nameMin), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(nameMax), - "platform_id": config.StringVariable(platformIdMax), - "quota_id": config.StringVariable(quotaIdMax), - "suspended": config.BoolVariable(suspendedMax), - "region": config.StringVariable(region), -} - -func testScfOrgConfigVarsMinUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMin)) - maps.Copy(tempConfig, testConfigVarsMin) - // update scf organization to a new name - tempConfig["name"] = config.StringVariable(nameMinUpdated) - return tempConfig -} - -func testScfOrgConfigVarsMaxUpdated() config.Variables { - tempConfig := make(config.Variables, len(testConfigVarsMax)) - maps.Copy(tempConfig, testConfigVarsMax) - // update scf organization to a new name, unsuspend it and assign a new quota - tempConfig["name"] = config.StringVariable(nameMaxUpdated) - tempConfig["quota_id"] = config.StringVariable(quotaIdMaxUpdated) - tempConfig["suspended"] = config.BoolVariable(!suspendedMax) - return tempConfig -} - -func TestAccScfOrganizationMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckScfOrganizationDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMin, - Config: testutil.ScfProviderConfig() + resourceMin, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "platform_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "quota_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "status"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "suspended"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "org_id"), - resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "user_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "username"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "password"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "created_at"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "updated_at"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMin, - Config: fmt.Sprintf(` - %s - data "stackit_scf_organization" "org" { - project_id = stackit_scf_organization.org.project_id - org_id = stackit_scf_organization.org.org_id - } - data "stackit_scf_organization_manager" "orgmanager" { - org_id = stackit_scf_organization.org.org_id - project_id = stackit_scf_organization.org.project_id - } - `, testutil.ScfProviderConfig()+resourceMin, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "project_id", - "data.stackit_scf_organization.org", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "created_at", - "data.stackit_scf_organization.org", "created_at", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "name", - "data.stackit_scf_organization.org", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "platform_id", - "data.stackit_scf_organization.org", "platform_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "org_id", - "data.stackit_scf_organization.org", "org_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "quota_id", - "data.stackit_scf_organization.org", "quota_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "region", - "data.stackit_scf_organization.org", "region", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "status", - "data.stackit_scf_organization.org", "status", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "suspended", - "data.stackit_scf_organization.org", "suspended", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "updated_at", - "data.stackit_scf_organization.org", "updated_at", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "region", - "data.stackit_scf_organization_manager.orgmanager", "region", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "platform_id", - "data.stackit_scf_organization_manager.orgmanager", "platform_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "project_id", - "data.stackit_scf_organization_manager.orgmanager", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "org_id", - "data.stackit_scf_organization_manager.orgmanager", "org_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "user_id"), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "username"), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_scf_organization.org", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_scf_organization.org"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_scf_organization.org") - } - orgId, ok := r.Primary.Attributes["org_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute org_id") - } - regionInAttributes, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, regionInAttributes, orgId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testScfOrgConfigVarsMinUpdated(), - Config: testutil.ScfProviderConfig() + resourceMin, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testScfOrgConfigVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testScfOrgConfigVarsMinUpdated()["name"])), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "platform_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "quota_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "suspended"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccScfOrgMax(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckScfOrganizationDestroy, - Steps: []resource.TestStep{ - // Creation - { - ConfigVariables: testConfigVarsMax, - Config: testutil.ScfProviderConfig() + resourceMax, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "quota_id", testutil.ConvertConfigVariable(testConfigVarsMax["quota_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "suspended", testutil.ConvertConfigVariable(testConfigVarsMax["suspended"])), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"), - resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])), - resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "display_name", platformName), - resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "system_id", platformSystemId), - resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "api_url", platformApiUrl), - resource.TestCheckResourceAttr("data.stackit_scf_platform.scf_platform", "console_url", platformConsoleUrl), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "org_id"), - resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization_manager.orgmanager", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "user_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "username"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "password"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "created_at"), - resource.TestCheckResourceAttrSet("stackit_scf_organization_manager.orgmanager", "updated_at"), - ), - }, - // Data source - { - ConfigVariables: testConfigVarsMax, - Config: fmt.Sprintf(` - %s - data "stackit_scf_organization" "org" { - project_id = stackit_scf_organization.org.project_id - org_id = stackit_scf_organization.org.org_id - region = var.region - } - data "stackit_scf_organization_manager" "orgmanager" { - org_id = stackit_scf_organization.org.org_id - project_id = stackit_scf_organization.org.project_id - } - data "stackit_scf_platform" "platform" { - platform_id = stackit_scf_organization.org.platform_id - project_id = stackit_scf_organization.org.project_id - } - `, testutil.ScfProviderConfig()+resourceMax, - ), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "project_id", - "data.stackit_scf_organization.org", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "created_at", - "data.stackit_scf_organization.org", "created_at", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "name", - "data.stackit_scf_organization.org", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "platform_id", - "data.stackit_scf_organization.org", "platform_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "org_id", - "data.stackit_scf_organization.org", "org_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "quota_id", - "data.stackit_scf_organization.org", "quota_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "region", - "data.stackit_scf_organization.org", "region", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "status", - "data.stackit_scf_organization.org", "status", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "suspended", - "data.stackit_scf_organization.org", "suspended", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "updated_at", - "data.stackit_scf_organization.org", "updated_at", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "platform_id", - "data.stackit_scf_platform.platform", "platform_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "project_id", - "data.stackit_scf_platform.platform", "project_id", - ), - resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "display_name", platformName), - resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "system_id", platformSystemId), - resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "display_name", platformName), - resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "region", region), - resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "api_url", platformApiUrl), - resource.TestCheckResourceAttr("data.stackit_scf_platform.platform", "console_url", platformConsoleUrl), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "region", - "data.stackit_scf_organization_manager.orgmanager", "region", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "platform_id", - "data.stackit_scf_organization_manager.orgmanager", "platform_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "project_id", - "data.stackit_scf_organization_manager.orgmanager", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_scf_organization.org", "org_id", - "data.stackit_scf_organization_manager.orgmanager", "org_id", - ), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "user_id"), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "username"), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "created_at"), - resource.TestCheckResourceAttrSet("data.stackit_scf_organization_manager.orgmanager", "updated_at"), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_scf_organization.org", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_scf_organization.org"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_scf_organization.org") - } - orgId, ok := r.Primary.Attributes["org_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute org_id") - } - regionInAttributes, ok := r.Primary.Attributes["region"] - if !ok { - return "", fmt.Errorf("couldn't find attribute region") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, regionInAttributes, orgId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: testScfOrgConfigVarsMaxUpdated(), - Config: testutil.ScfProviderConfig() + resourceMax, - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_scf_organization.org", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "name", testutil.ConvertConfigVariable(testScfOrgConfigVarsMaxUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "platform_id", testutil.ConvertConfigVariable(testConfigVarsMax["platform_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "quota_id", testutil.ConvertConfigVariable(testScfOrgConfigVarsMaxUpdated()["quota_id"])), - resource.TestCheckResourceAttr("stackit_scf_organization.org", "suspended", testutil.ConvertConfigVariable(testScfOrgConfigVarsMaxUpdated()["suspended"])), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "created_at"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "org_id"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "region"), - resource.TestCheckResourceAttrSet("stackit_scf_organization.org", "updated_at"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckScfOrganizationDestroy(s *terraform.State) error { - ctx := context.Background() - var client *scf.APIClient - var err error - - if testutil.ScfCustomEndpoint == "" { - client, err = scf.NewAPIClient() - } else { - client, err = scf.NewAPIClient( - stackitSdkConfig.WithEndpoint(testutil.ScfCustomEndpoint), - ) - } - - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var orgsToDestroy []string - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_scf_organization" { - continue - } - orgId := strings.Split(rs.Primary.ID, core.Separator)[1] - orgsToDestroy = append(orgsToDestroy, orgId) - } - - organizationsList, err := client.ListOrganizations(ctx, testutil.ProjectId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting scf organizations: %w", err) - } - - scfOrgs := organizationsList.GetResources() - for i := range scfOrgs { - if scfOrgs[i].Guid == nil { - continue - } - if utils.Contains(orgsToDestroy, *scfOrgs[i].Guid) { - _, err := client.DeleteOrganizationExecute(ctx, testutil.ProjectId, testutil.Region, *scfOrgs[i].Guid) - if err != nil { - return fmt.Errorf("destroying scf organization %s during CheckDestroy: %w", *scfOrgs[i].Guid, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/scf/testdata/resource-max.tf b/stackit/internal/services/scf/testdata/resource-max.tf deleted file mode 100644 index c7f3ab5f..00000000 --- a/stackit/internal/services/scf/testdata/resource-max.tf +++ /dev/null @@ -1,23 +0,0 @@ - -variable "project_id" {} -variable "name" {} -variable "quota_id" {} -variable "suspended" {} -variable "region" {} - -resource "stackit_scf_organization" "org" { - project_id = var.project_id - name = var.name - suspended = var.suspended - quota_id = var.quota_id - region = var.region -} - -resource "stackit_scf_organization_manager" "orgmanager" { - project_id = var.project_id - org_id = stackit_scf_organization.org.org_id -} -data "stackit_scf_platform" "scf_platform" { - project_id = var.project_id - platform_id = stackit_scf_organization.org.platform_id -} \ No newline at end of file diff --git a/stackit/internal/services/scf/testdata/resource-min.tf b/stackit/internal/services/scf/testdata/resource-min.tf deleted file mode 100644 index f2d11ef3..00000000 --- a/stackit/internal/services/scf/testdata/resource-min.tf +++ /dev/null @@ -1,13 +0,0 @@ - -variable "project_id" {} -variable "name" {} - -resource "stackit_scf_organization" "org" { - project_id = var.project_id - name = var.name -} - -resource "stackit_scf_organization_manager" "orgmanager" { - project_id = var.project_id - org_id = stackit_scf_organization.org.org_id -} \ No newline at end of file diff --git a/stackit/internal/services/scf/utils/utils.go b/stackit/internal/services/scf/utils/utils.go deleted file mode 100644 index 347e109b..00000000 --- a/stackit/internal/services/scf/utils/utils.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *scf.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ScfCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ScfCustomEndpoint)) - } - apiClient, err := scf.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/scf/utils/utils_test.go b/stackit/internal/services/scf/utils/utils_test.go deleted file mode 100644 index 1a77a0ae..00000000 --- a/stackit/internal/services/scf/utils/utils_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "0.8.15" - testCustomEndpoint = "https://scf-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *scf.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *scf.APIClient { - apiClient, err := scf.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ScfCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *scf.APIClient { - apiClient, err := scf.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/secretsmanager/instance/datasource.go b/stackit/internal/services/secretsmanager/instance/datasource.go deleted file mode 100644 index d17f4001..00000000 --- a/stackit/internal/services/secretsmanager/instance/datasource.go +++ /dev/null @@ -1,158 +0,0 @@ -package secretsmanager - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *secretsmanager.APIClient -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_secretsmanager_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Secrets Manager instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Secrets Manager instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the Secrets Manager instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acls": "The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "acls": schema.SetAttribute{ - Description: descriptions["acls"], - ElementType: types.StringType, - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) - return - } - - err = mapFields(instanceResp, aclList, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager instance read") -} diff --git a/stackit/internal/services/secretsmanager/instance/resource.go b/stackit/internal/services/secretsmanager/instance/resource.go deleted file mode 100644 index 576da2f3..00000000 --- a/stackit/internal/services/secretsmanager/instance/resource.go +++ /dev/null @@ -1,481 +0,0 @@ -package secretsmanager - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - ACLs types.Set `tfsdk:"acls"` -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *secretsmanager.APIClient -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_secretsmanager_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Secrets Manager instance client configured") -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Secrets Manager instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`\".", - "instance_id": "ID of the Secrets Manager instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acls": "The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "acls": schema.SetAttribute{ - Description: descriptions["acls"], - ElementType: types.StringType, - Optional: true, - Validators: []validator.Set{ - setvalidator.ValueStringsAre( - validate.CIDR(), - ), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - - var acls []string - if !(model.ACLs.IsNull() || model.ACLs.IsUnknown()) { - diags = model.ACLs.ElementsAs(ctx, &acls, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.Id - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Create ACLs - err = updateACLs(ctx, projectId, instanceId, acls, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating ACLs: %v", err)) - return - } - aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) - return - } - - // Map response body to schema - err = mapFields(createResp, aclList, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) - return - } - - // Map response body to schema - err = mapFields(instanceResp, aclList, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - var acls []string - if !(model.ACLs.IsNull() || model.ACLs.IsUnknown()) { - diags = model.ACLs.ElementsAs(ctx, &acls, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Update ACLs - err := updateACLs(ctx, projectId, instanceId, acls, r.client) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Updating ACLs: %v", err)) - return - } - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) - return - } - - // Map response body to schema - err = mapFields(instanceResp, aclList, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Secrets Manager instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - tflog.Info(ctx, "Secrets Manager instance state imported") -} - -func mapFields(instance *secretsmanager.Instance, aclList *secretsmanager.ListACLsResponse, model *Model) error { - if instance == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.Id != nil { - instanceId = *instance.Id - } else { - return fmt.Errorf("instance id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), instanceId) - model.InstanceId = types.StringValue(instanceId) - model.Name = types.StringPointerValue(instance.Name) - - err := mapACLs(aclList, model) - if err != nil { - return err - } - - return nil -} - -func mapACLs(aclList *secretsmanager.ListACLsResponse, model *Model) error { - if aclList == nil { - return fmt.Errorf("nil ACL list") - } - if aclList.Acls == nil || len(*aclList.Acls) == 0 { - model.ACLs = types.SetNull(types.StringType) - return nil - } - - acls := []attr.Value{} - for _, acl := range *aclList.Acls { - acls = append(acls, types.StringValue(*acl.Cidr)) - } - aclsList, diags := types.SetValue(types.StringType, acls) - if diags.HasError() { - return fmt.Errorf("mapping ACLs: %w", core.DiagsToError(diags)) - } - model.ACLs = aclsList - return nil -} - -func toCreatePayload(model *Model) (*secretsmanager.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - return &secretsmanager.CreateInstancePayload{ - Name: conversion.StringValueToPointer(model.Name), - }, nil -} - -// updateACLs creates and deletes ACLs so that the instance's ACLs are the ones in the model -func updateACLs(ctx context.Context, projectId, instanceId string, acls []string, client *secretsmanager.APIClient) error { - // Get ACLs current state - currentACLsResp, err := client.ListACLs(ctx, projectId, instanceId).Execute() - if err != nil { - return fmt.Errorf("fetching current ACLs: %w", err) - } - - type aclState struct { - isInModel bool - isCreated bool - id string - } - aclsState := make(map[string]*aclState) - for _, cidr := range acls { - aclsState[cidr] = &aclState{ - isInModel: true, - } - } - for _, acl := range *currentACLsResp.Acls { - cidr := *acl.Cidr - if _, ok := aclsState[cidr]; !ok { - aclsState[cidr] = &aclState{} - } - aclsState[cidr].isCreated = true - aclsState[cidr].id = *acl.Id - } - - // Create/delete ACLs - for cidr, state := range aclsState { - if state.isInModel && !state.isCreated { - payload := secretsmanager.CreateACLPayload{ - Cidr: sdkUtils.Ptr(cidr), - } - _, err := client.CreateACL(ctx, projectId, instanceId).CreateACLPayload(payload).Execute() - if err != nil { - return fmt.Errorf("creating ACL '%v': %w", cidr, err) - } - } - - if !state.isInModel && state.isCreated { - err := client.DeleteACL(ctx, projectId, instanceId, state.id).Execute() - if err != nil { - return fmt.Errorf("deleting ACL '%v': %w", cidr, err) - } - } - } - - return nil -} diff --git a/stackit/internal/services/secretsmanager/instance/resource_test.go b/stackit/internal/services/secretsmanager/instance/resource_test.go deleted file mode 100644 index 39d2df83..00000000 --- a/stackit/internal/services/secretsmanager/instance/resource_test.go +++ /dev/null @@ -1,487 +0,0 @@ -package secretsmanager - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/gorilla/mux" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *secretsmanager.Instance - ListACLsResponse *secretsmanager.ListACLsResponse - expected Model - isValid bool - }{ - { - "default_values", - &secretsmanager.Instance{}, - &secretsmanager.ListACLsResponse{}, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - ACLs: types.SetNull(types.StringType), - }, - true, - }, - { - "simple_values", - &secretsmanager.Instance{ - Name: utils.Ptr("name"), - }, - &secretsmanager.ListACLsResponse{ - Acls: &[]secretsmanager.ACL{ - { - Cidr: utils.Ptr("cidr-1"), - Id: utils.Ptr("id-cidr-1"), - }, - { - Cidr: utils.Ptr("cidr-2"), - Id: utils.Ptr("id-cidr-2"), - }, - { - Cidr: utils.Ptr("cidr-3"), - Id: utils.Ptr("id-cidr-3"), - }, - }, - }, - Model{ - Id: types.StringValue("pid,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - ACLs: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("cidr-1"), - types.StringValue("cidr-2"), - types.StringValue("cidr-3"), - }), - }, - true, - }, - { - "nil_response", - nil, - &secretsmanager.ListACLsResponse{}, - Model{}, - false, - }, - { - "nil_acli_list", - &secretsmanager.Instance{}, - nil, - Model{}, - false, - }, - { - "no_resource_id", - &secretsmanager.Instance{}, - &secretsmanager.ListACLsResponse{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFields(tt.input, tt.ListACLsResponse, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *secretsmanager.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &secretsmanager.CreateInstancePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - }, - &secretsmanager.CreateInstancePayload{ - Name: utils.Ptr("name"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - }, - &secretsmanager.CreateInstancePayload{ - Name: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestUpdateACLs(t *testing.T) { - // This is the response used when getting all ACLs currently, across all tests - getAllACLsResp := secretsmanager.ListACLsResponse{ - Acls: &[]secretsmanager.ACL{ - { - Cidr: utils.Ptr("acl-1"), - Id: utils.Ptr("id-acl-1"), - }, - { - Cidr: utils.Ptr("acl-2"), - Id: utils.Ptr("id-acl-2"), - }, - { - Cidr: utils.Ptr("acl-3"), - Id: utils.Ptr("id-acl-3"), - }, - { - Cidr: utils.Ptr("acl-2"), - Id: utils.Ptr("id-acl-2-repeated"), - }, - }, - } - getAllACLsRespBytes, err := json.Marshal(getAllACLsResp) - if err != nil { - t.Fatalf("Failed to marshal get all ACLs response: %v", err) - } - - // This is the response used whenever an API returns a failure response - failureRespBytes := []byte("{\"message\": \"Something bad happened\"") - - tests := []struct { - description string - acls []string - getAllACLsFails bool - createACLFails bool - deleteACLFails bool - isValid bool - expectedACLsStates map[string]bool // Keys are CIDR; value is true if CIDR should exist at the end, false if should be deleted - }{ - { - description: "no_changes", - acls: []string{"acl-3", "acl-2", "acl-1"}, - expectedACLsStates: map[string]bool{ - "acl-1": true, - "acl-2": true, - "acl-3": true, - }, - isValid: true, - }, - { - description: "create_acl", - acls: []string{"acl-1", "acl-2", "acl-3", "acl-4"}, - expectedACLsStates: map[string]bool{ - "acl-1": true, - "acl-2": true, - "acl-3": true, - "acl-4": true, - }, - isValid: true, - }, - { - description: "delete_acl", - acls: []string{"acl-3", "acl-1"}, - expectedACLsStates: map[string]bool{ - "acl-1": true, - "acl-2": false, - "acl-3": true, - }, - isValid: true, - }, - { - description: "multiple_changes", - acls: []string{"acl-4", "acl-3", "acl-1", "acl-5"}, - expectedACLsStates: map[string]bool{ - "acl-1": true, - "acl-2": false, - "acl-3": true, - "acl-4": true, - "acl-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_repetition", - acls: []string{"acl-4", "acl-3", "acl-1", "acl-5", "acl-5"}, - expectedACLsStates: map[string]bool{ - "acl-1": true, - "acl-2": false, - "acl-3": true, - "acl-4": true, - "acl-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_2", - acls: []string{"acl-4", "acl-5"}, - expectedACLsStates: map[string]bool{ - "acl-1": false, - "acl-2": false, - "acl-3": false, - "acl-4": true, - "acl-5": true, - }, - isValid: true, - }, - { - description: "multiple_changes_3", - acls: []string{}, - expectedACLsStates: map[string]bool{ - "acl-1": false, - "acl-2": false, - "acl-3": false, - }, - isValid: true, - }, - { - description: "get_fails", - acls: []string{"acl-1", "acl-2", "acl-3"}, - getAllACLsFails: true, - isValid: false, - }, - { - description: "create_fails_1", - acls: []string{"acl-1", "acl-2", "acl-3", "acl-4"}, - createACLFails: true, - isValid: false, - }, - { - description: "create_fails_2", - acls: []string{"acl-1", "acl-2"}, - createACLFails: true, - expectedACLsStates: map[string]bool{ - "acl-1": true, - "acl-2": true, - "acl-3": false, - }, - isValid: true, - }, - { - description: "delete_fails_1", - acls: []string{"acl-1", "acl-2"}, - deleteACLFails: true, - isValid: false, - }, - { - description: "delete_fails_2", - acls: []string{"acl-1", "acl-2", "acl-3", "acl-4"}, - deleteACLFails: true, - expectedACLsStates: map[string]bool{ - "acl-1": true, - "acl-2": true, - "acl-3": true, - "acl-4": true, - }, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - // Will be compared to tt.expectedACLsStates at the end - aclsStates := make(map[string]bool) - aclsStates["acl-1"] = true - aclsStates["acl-2"] = true - aclsStates["acl-3"] = true - - // Handler for getting all ACLs - getAllACLsHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - if tt.getAllACLsFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Get all ACLs handler: failed to write bad response: %v", err) - } - return - } - - _, err := w.Write(getAllACLsRespBytes) - if err != nil { - t.Errorf("Get all ACLs handler: failed to write response: %v", err) - } - }) - - // Handler for creating ACL - createACLHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - decoder := json.NewDecoder(r.Body) - var payload secretsmanager.CreateACLPayload - err := decoder.Decode(&payload) - if err != nil { - t.Errorf("Create ACL handler: failed to parse payload") - return - } - if payload.Cidr == nil { - t.Errorf("Create ACL handler: nil CIDR") - return - } - cidr := *payload.Cidr - if cidrExists, cidrWasCreated := aclsStates[cidr]; cidrWasCreated && cidrExists { - t.Errorf("Create ACL handler: attempted to create CIDR '%v' that already exists", *payload.Cidr) - return - } - - w.Header().Set("Content-Type", "application/json") - if tt.createACLFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Create ACL handler: failed to write bad response: %v", err) - } - return - } - - resp := secretsmanager.ACL{ - Cidr: utils.Ptr(cidr), - Id: utils.Ptr(fmt.Sprintf("id-%s", cidr)), - } - respBytes, err := json.Marshal(resp) - if err != nil { - t.Errorf("Create ACL handler: failed to marshal response: %v", err) - return - } - _, err = w.Write(respBytes) - if err != nil { - t.Errorf("Create ACL handler: failed to write response: %v", err) - } - aclsStates[cidr] = true - }) - - // Handler for deleting ACL - deleteACLHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - aclId, ok := vars["aclId"] - if !ok { - t.Errorf("Delete ACL handler: no ACL ID") - return - } - cidr, ok := strings.CutPrefix(aclId, "id-") - if !ok { - t.Errorf("Delete ACL handler: got unexpected ACL ID '%v'", aclId) - return - } - cidr, _ = strings.CutSuffix(cidr, "-repeated") - cidrExists, cidrWasCreated := aclsStates[cidr] - if !cidrWasCreated { - t.Errorf("Delete ACL handler: attempted to delete CIDR '%v' that wasn't created", cidr) - return - } - if cidrWasCreated && !cidrExists { - t.Errorf("Delete ACL handler: attempted to delete CIDR '%v' that was already deleted", cidr) - return - } - - w.Header().Set("Content-Type", "application/json") - if tt.deleteACLFails { - w.WriteHeader(http.StatusInternalServerError) - _, err := w.Write(failureRespBytes) - if err != nil { - t.Errorf("Delete ACL handler: failed to write bad response: %v", err) - } - return - } - - _, err = w.Write([]byte("{}")) - if err != nil { - t.Errorf("Delete ACL handler: failed to write response: %v", err) - } - aclsStates[cidr] = false - }) - - // Setup server and client - router := mux.NewRouter() - router.HandleFunc("/v1/projects/{projectId}/instances/{instanceId}/acls", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - getAllACLsHandler(w, r) - } else if r.Method == "POST" { - createACLHandler(w, r) - } - }) - router.HandleFunc("/v1/projects/{projectId}/instances/{instanceId}/acls/{aclId}", deleteACLHandler) - mockedServer := httptest.NewServer(router) - defer mockedServer.Close() - client, err := secretsmanager.NewAPIClient( - config.WithEndpoint(mockedServer.URL), - config.WithoutAuthentication(), - ) - if err != nil { - t.Fatalf("Failed to initialize client: %v", err) - } - - // Run test - err = updateACLs(context.Background(), "pid", "iid", tt.acls, client) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(aclsStates, tt.expectedACLsStates) - if diff != "" { - t.Fatalf("ACL states do not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go deleted file mode 100644 index 16e6c110..00000000 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ /dev/null @@ -1,414 +0,0 @@ -package secretsmanager_test - -import ( - "context" - _ "embed" - "fmt" - "maps" - "regexp" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - core_config "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/resource-min.tf - resourceMinConfig string - - //go:embed testdata/resource-max.tf - resourceMaxConfig string -) - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "instance_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "user_description": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "write_enabled": config.BoolVariable(true), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "instance_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "user_description": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "acl1": config.StringVariable("10.100.0.0/24"), - "acl2": config.StringVariable("10.100.1.0/24"), - "write_enabled": config.BoolVariable(true), -} - -func configVarsInvalid(vars config.Variables) config.Variables { - tempConfig := maps.Clone(vars) - delete(tempConfig, "instance_name") - return tempConfig -} - -func configVarsMinUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMin) - tempConfig["write_enabled"] = config.BoolVariable(false) - return tempConfig -} - -func configVarsMaxUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMax) - tempConfig["write_enabled"] = config.BoolVariable(false) - tempConfig["acl2"] = config.StringVariable("10.100.2.0/24") - return tempConfig -} - -func TestAccSecretsManagerMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSecretsManagerDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: testutil.SecretsManagerProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: configVarsInvalid(testConfigVarsMin), - ExpectError: regexp.MustCompile(`input variable "instance_name" is not set,`), - }, - // Creation - { - Config: resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["instance_name"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "0"), - - // User - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "project_id", - "stackit_secretsmanager_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "instance_id", - "stackit_secretsmanager_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "user_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "description", testutil.ConvertConfigVariable(testConfigVarsMin["user_description"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "write_enabled", testutil.ConvertConfigVariable(testConfigVarsMin["write_enabled"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "username"), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "password"), - ), - }, - // Data source - { - Config: resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_instance.instance", "instance_id", - "data.stackit_secretsmanager_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["instance_name"])), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.#", "0"), - - // User - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "project_id", - "data.stackit_secretsmanager_user.user", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "instance_id", - "data.stackit_secretsmanager_user.user", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "user_id", - "data.stackit_secretsmanager_user.user", "user_id", - ), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_user.user", "description", testutil.ConvertConfigVariable(testConfigVarsMin["user_description"])), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_user.user", "write_enabled", testutil.ConvertConfigVariable(testConfigVarsMin["write_enabled"])), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "username", - "data.stackit_secretsmanager_user.user", "username", - ), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_secretsmanager_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_secretsmanager_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_secretsmanager_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - Config: resourceMinConfig, - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_secretsmanager_user.user", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_secretsmanager_user.user"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_secretsmanager_user.user") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - userId, ok := r.Primary.Attributes["user_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute user_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, userId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password"}, - Check: resource.TestCheckNoResourceAttr("stackit_secretsmanager_user.user", "password"), - }, - // Update - { - Config: resourceMinConfig, - ConfigVariables: configVarsMinUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", testutil.ConvertConfigVariable(configVarsMinUpdated()["instance_name"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "0"), - - // User - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "project_id", - "stackit_secretsmanager_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "instance_id", - "stackit_secretsmanager_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "user_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "description", testutil.ConvertConfigVariable(configVarsMinUpdated()["user_description"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "write_enabled", testutil.ConvertConfigVariable(configVarsMinUpdated()["write_enabled"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "username"), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "password"), - ), - }, - - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccSecretsManagerMax(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSecretsManagerDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: testutil.SecretsManagerProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: configVarsInvalid(testConfigVarsMax), - ExpectError: regexp.MustCompile(`input variable "instance_name" is not set,`), - }, - // Creation - { - Config: resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["instance_name"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "2"), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl1"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.1", testutil.ConvertConfigVariable(testConfigVarsMax["acl2"])), - - // User - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "project_id", - "stackit_secretsmanager_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "instance_id", - "stackit_secretsmanager_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "user_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "description", testutil.ConvertConfigVariable(testConfigVarsMax["user_description"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "write_enabled", testutil.ConvertConfigVariable(testConfigVarsMax["write_enabled"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "username"), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "password"), - ), - }, - // Data source - { - Config: resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_instance.instance", "instance_id", - "data.stackit_secretsmanager_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["instance_name"])), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.#", "2"), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl1"])), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.1", testutil.ConvertConfigVariable(testConfigVarsMax["acl2"])), - - // User - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "project_id", - "data.stackit_secretsmanager_user.user", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "instance_id", - "data.stackit_secretsmanager_user.user", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "user_id", - "data.stackit_secretsmanager_user.user", "user_id", - ), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_user.user", "description", testutil.ConvertConfigVariable(testConfigVarsMax["user_description"])), - resource.TestCheckResourceAttr("data.stackit_secretsmanager_user.user", "write_enabled", testutil.ConvertConfigVariable(testConfigVarsMax["write_enabled"])), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "username", - "data.stackit_secretsmanager_user.user", "username", - ), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_secretsmanager_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_secretsmanager_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_secretsmanager_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - { - Config: resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_secretsmanager_user.user", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_secretsmanager_user.user"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_secretsmanager_user.user") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - userId, ok := r.Primary.Attributes["user_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute user_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, userId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password"}, - Check: resource.TestCheckNoResourceAttr("stackit_secretsmanager_user.user", "password"), - }, - // Update - { - Config: resourceMaxConfig, - ConfigVariables: configVarsMaxUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["instance_name"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "2"), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["acl1"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.1", testutil.ConvertConfigVariable(configVarsMaxUpdated()["acl2"])), - - // User - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "project_id", - "stackit_secretsmanager_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_secretsmanager_user.user", "instance_id", - "stackit_secretsmanager_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "user_id"), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "description", testutil.ConvertConfigVariable(configVarsMaxUpdated()["user_description"])), - resource.TestCheckResourceAttr("stackit_secretsmanager_user.user", "write_enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["write_enabled"])), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "username"), - resource.TestCheckResourceAttrSet("stackit_secretsmanager_user.user", "password"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckSecretsManagerDestroy(s *terraform.State) error { - ctx := context.Background() - var client *secretsmanager.APIClient - var err error - if testutil.SecretsManagerCustomEndpoint == "" { - client, err = secretsmanager.NewAPIClient( - core_config.WithRegion("eu01"), - ) - } else { - client, err = secretsmanager.NewAPIClient( - core_config.WithEndpoint(testutil.SecretsManagerCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_secretsmanager_instance" { - continue - } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := *instancesResp.Instances - for i := range instances { - if instances[i].Id == nil { - continue - } - if utils.Contains(instancesToDestroy, *instances[i].Id) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].Id) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/secretsmanager/testdata/resource-max.tf b/stackit/internal/services/secretsmanager/testdata/resource-max.tf deleted file mode 100644 index deea23ba..00000000 --- a/stackit/internal/services/secretsmanager/testdata/resource-max.tf +++ /dev/null @@ -1,34 +0,0 @@ -variable "project_id" {} -variable "instance_name" {} -variable "user_description" {} -variable "write_enabled" {} -variable "acl1" {} -variable "acl2" {} - -resource "stackit_secretsmanager_instance" "instance" { - project_id = var.project_id - name = var.instance_name - acls = [ - var.acl1, - var.acl2, - ] -} - -resource "stackit_secretsmanager_user" "user" { - project_id = var.project_id - instance_id = stackit_secretsmanager_instance.instance.instance_id - description = var.user_description - write_enabled = var.write_enabled -} - - -data "stackit_secretsmanager_instance" "instance" { - project_id = var.project_id - instance_id = stackit_secretsmanager_instance.instance.instance_id -} - -data "stackit_secretsmanager_user" "user" { - project_id = var.project_id - instance_id = stackit_secretsmanager_instance.instance.instance_id - user_id = stackit_secretsmanager_user.user.user_id -} diff --git a/stackit/internal/services/secretsmanager/testdata/resource-min.tf b/stackit/internal/services/secretsmanager/testdata/resource-min.tf deleted file mode 100644 index 834c043e..00000000 --- a/stackit/internal/services/secretsmanager/testdata/resource-min.tf +++ /dev/null @@ -1,28 +0,0 @@ -variable "project_id" {} -variable "instance_name" {} -variable "user_description" {} -variable "write_enabled" {} - -resource "stackit_secretsmanager_instance" "instance" { - project_id = var.project_id - name = var.instance_name -} - -resource "stackit_secretsmanager_user" "user" { - project_id = var.project_id - instance_id = stackit_secretsmanager_instance.instance.instance_id - description = var.user_description - write_enabled = var.write_enabled -} - - -data "stackit_secretsmanager_instance" "instance" { - project_id = var.project_id - instance_id = stackit_secretsmanager_instance.instance.instance_id -} - -data "stackit_secretsmanager_user" "user" { - project_id = var.project_id - instance_id = stackit_secretsmanager_instance.instance.instance_id - user_id = stackit_secretsmanager_user.user.user_id -} diff --git a/stackit/internal/services/secretsmanager/user/datasource.go b/stackit/internal/services/secretsmanager/user/datasource.go deleted file mode 100644 index e3b40f59..00000000 --- a/stackit/internal/services/secretsmanager/user/datasource.go +++ /dev/null @@ -1,203 +0,0 @@ -package secretsmanager - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &userDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Description types.String `tfsdk:"description"` - WriteEnabled types.Bool `tfsdk:"write_enabled"` - Username types.String `tfsdk:"username"` -} - -// NewUserDataSource is a helper function to simplify the provider implementation. -func NewUserDataSource() datasource.DataSource { - return &userDataSource{} -} - -// userDataSource is the data source implementation. -type userDataSource struct { - client *secretsmanager.APIClient -} - -// Metadata returns the data source type name. -func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_secretsmanager_user" -} - -// Configure adds the provider configured client to the data source. -func (r *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Secrets Manager user client configured") -} - -// Schema defines the schema for the data source. -func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Secrets Manager user data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source identifier. It is structured as \"`project_id`,`instance_id`,`user_id`\".", - "user_id": "The user's ID.", - "instance_id": "ID of the Secrets Manager instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "description": "A user chosen description to differentiate between multiple users. Can't be changed after creation.", - "write_enabled": "If true, the user has writeaccess to the secrets engine.", - "username": "An auto-generated user name.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "description": schema.StringAttribute{ - Description: descriptions["description"], - Computed: true, - }, - "write_enabled": schema.BoolAttribute{ - Description: descriptions["write_enabled"], - Computed: true, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Computed: true, - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - userResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading user", - fmt.Sprintf("User with ID %q or instance with ID %q does not exist in project %q.", userId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema and populate Computed attribute values - err = mapDataSourceFields(userResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager user read") -} - -func mapDataSourceFields(user *secretsmanager.User, model *DataSourceModel) error { - if user == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), userId) - model.UserId = types.StringValue(userId) - model.Description = types.StringPointerValue(user.Description) - model.WriteEnabled = types.BoolPointerValue(user.Write) - model.Username = types.StringPointerValue(user.Username) - return nil -} diff --git a/stackit/internal/services/secretsmanager/user/datasource_test.go b/stackit/internal/services/secretsmanager/user/datasource_test.go deleted file mode 100644 index 7ecfaba4..00000000 --- a/stackit/internal/services/secretsmanager/user/datasource_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package secretsmanager - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" -) - -func TestMapDataSourceFields(t *testing.T) { - tests := []struct { - description string - input *secretsmanager.User - expected DataSourceModel - isValid bool - }{ - { - "default_values", - &secretsmanager.User{ - Id: utils.Ptr("uid"), - }, - DataSourceModel{ - Id: types.StringValue("pid,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Description: types.StringNull(), - WriteEnabled: types.BoolNull(), - Username: types.StringNull(), - }, - true, - }, - { - "simple_values", - &secretsmanager.User{ - Id: utils.Ptr("uid"), - Description: utils.Ptr("description"), - Write: utils.Ptr(false), - Username: utils.Ptr("username"), - }, - DataSourceModel{ - Id: types.StringValue("pid,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Description: types.StringValue("description"), - WriteEnabled: types.BoolValue(false), - Username: types.StringValue("username"), - }, - true, - }, - { - "nil_response", - nil, - DataSourceModel{}, - false, - }, - { - "no_resource_id", - &secretsmanager.User{}, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &DataSourceModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapDataSourceFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/secretsmanager/user/resource.go b/stackit/internal/services/secretsmanager/user/resource.go deleted file mode 100644 index b76fce38..00000000 --- a/stackit/internal/services/secretsmanager/user/resource.go +++ /dev/null @@ -1,414 +0,0 @@ -package secretsmanager - -import ( - "context" - "fmt" - "net/http" - "strings" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} - _ resource.ResourceWithImportState = &userResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Description types.String `tfsdk:"description"` - WriteEnabled types.Bool `tfsdk:"write_enabled"` - Username types.String `tfsdk:"username"` - Password types.String `tfsdk:"password"` -} - -// NewUserResource is a helper function to simplify the provider implementation. -func NewUserResource() resource.Resource { - return &userResource{} -} - -// userResource is the resource implementation. -type userResource struct { - client *secretsmanager.APIClient -} - -// Metadata returns the resource type name. -func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_secretsmanager_user" -} - -// Configure adds the provider configured client to the resource. -func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Secrets Manager user client configured") -} - -// Schema defines the schema for the resource. -func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "Secrets Manager user resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`instance_id`,`user_id`\".", - "user_id": "The user's ID.", - "instance_id": "ID of the Secrets Manager instance.", - "project_id": "STACKIT Project ID to which the instance is associated.", - "description": "A user chosen description to differentiate between multiple users. Can't be changed after creation.", - "write_enabled": "If true, the user has writeaccess to the secrets engine.", - "username": "An auto-generated user name.", - "password": "An auto-generated password.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "description": schema.StringAttribute{ - Description: descriptions["description"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "write_enabled": schema.BoolAttribute{ - Description: descriptions["write_enabled"], - Required: true, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Computed: true, - }, - "password": schema.StringAttribute{ - Description: descriptions["password"], - Computed: true, - Sensitive: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - - // Generate API request body from model - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new user - userResp, err := r.client.CreateUser(ctx, projectId, instanceId).CreateUserPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if userResp.Id == nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "Got empty user id") - return - } - userId := *userResp.Id - ctx = tflog.SetField(ctx, "user_id", userId) - - // Map response body to schema - err = mapFields(userResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager user created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - userResp, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(userResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager user read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - // Generate API request body from model - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing user - err = r.client.UpdateUser(ctx, projectId, instanceId, userId).UpdateUserPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - user, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API to get user's current state: %v", err)) - return - } - - // Get existing state - diags = req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - // Map response body to schema - err = mapFields(user, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Secrets Manager user updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - - // Delete existing user - err := r.client.DeleteUser(ctx, projectId, instanceId, userId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Secrets Manager user deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,user_id -func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing credential", - fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[user_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[2])...) - core.LogAndAddWarning(ctx, &resp.Diagnostics, - "Secrets Manager user imported with empty password", - "The user password is not imported as it is only available upon creation of a new user. The password field will be empty.", - ) - tflog.Info(ctx, "Secrets Manager user state imported") -} - -func toCreatePayload(model *Model) (*secretsmanager.CreateUserPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - return &secretsmanager.CreateUserPayload{ - Description: conversion.StringValueToPointer(model.Description), - Write: conversion.BoolValueToPointer(model.WriteEnabled), - }, nil -} - -func toUpdatePayload(model *Model) (*secretsmanager.UpdateUserPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - return &secretsmanager.UpdateUserPayload{ - Write: conversion.BoolValueToPointer(model.WriteEnabled), - }, nil -} - -func mapFields(user *secretsmanager.User, model *Model) error { - if user == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), userId) - model.UserId = types.StringValue(userId) - model.Description = types.StringPointerValue(user.Description) - model.WriteEnabled = types.BoolPointerValue(user.Write) - model.Username = types.StringPointerValue(user.Username) - // Password only sent in creation response, responses after that have it as "" - if user.Password != nil && *user.Password != "" { - model.Password = types.StringPointerValue(user.Password) - } - return nil -} diff --git a/stackit/internal/services/secretsmanager/user/resource_test.go b/stackit/internal/services/secretsmanager/user/resource_test.go deleted file mode 100644 index 2555685c..00000000 --- a/stackit/internal/services/secretsmanager/user/resource_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package secretsmanager - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *secretsmanager.User - modelPassword *string - expected Model - isValid bool - }{ - { - "default_values", - &secretsmanager.User{ - Id: utils.Ptr("uid"), - }, - nil, - Model{ - Id: types.StringValue("pid,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Description: types.StringNull(), - WriteEnabled: types.BoolNull(), - Username: types.StringNull(), - Password: types.StringNull(), - }, - true, - }, - { - "simple_values", - &secretsmanager.User{ - Id: utils.Ptr("uid"), - Description: utils.Ptr("description"), - Write: utils.Ptr(false), - Username: utils.Ptr("username"), - Password: utils.Ptr("password"), - }, - nil, - Model{ - Id: types.StringValue("pid,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Description: types.StringValue("description"), - WriteEnabled: types.BoolValue(false), - Username: types.StringValue("username"), - Password: types.StringValue("password"), - }, - true, - }, - { - "nil_response", - nil, - nil, - Model{}, - false, - }, - { - "no_resource_id", - &secretsmanager.User{}, - nil, - Model{}, - false, - }, - { - "no_password_in_response_1", - &secretsmanager.User{ - Id: utils.Ptr("uid"), - Description: utils.Ptr("description"), - Write: utils.Ptr(false), - Username: utils.Ptr("username"), - }, - utils.Ptr("password"), - Model{ - Id: types.StringValue("pid,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Description: types.StringValue("description"), - WriteEnabled: types.BoolValue(false), - Username: types.StringValue("username"), - Password: types.StringValue("password"), - }, - true, - }, - { - "no_password_in_response_2", - &secretsmanager.User{ - Id: utils.Ptr("uid"), - Description: utils.Ptr("description"), - Write: utils.Ptr(false), - Username: utils.Ptr("username"), - Password: utils.Ptr(""), - }, - utils.Ptr("password"), - Model{ - Id: types.StringValue("pid,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Description: types.StringValue("description"), - WriteEnabled: types.BoolValue(false), - Username: types.StringValue("username"), - Password: types.StringValue("password"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - if tt.modelPassword != nil { - state.Password = types.StringPointerValue(tt.modelPassword) - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *secretsmanager.CreateUserPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &secretsmanager.CreateUserPayload{ - Description: nil, - Write: nil, - }, - true, - }, - { - "simple_values", - &Model{ - Description: types.StringValue("description"), - WriteEnabled: types.BoolValue(false), - }, - &secretsmanager.CreateUserPayload{ - Description: utils.Ptr("description"), - Write: utils.Ptr(false), - }, - true, - }, - { - "null_fields", - &Model{ - Description: types.StringNull(), - WriteEnabled: types.BoolNull(), - }, - &secretsmanager.CreateUserPayload{ - Description: nil, - Write: nil, - }, - true, - }, - { - "empty_fields", - &Model{ - Description: types.StringValue(""), - WriteEnabled: types.BoolNull(), - }, - &secretsmanager.CreateUserPayload{ - Description: utils.Ptr(""), - Write: nil, - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *secretsmanager.UpdateUserPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &secretsmanager.UpdateUserPayload{ - Write: nil, - }, - true, - }, - { - "simple_values", - &Model{ - WriteEnabled: types.BoolValue(false), - }, - &secretsmanager.UpdateUserPayload{ - Write: utils.Ptr(false), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/secretsmanager/utils/util.go b/stackit/internal/services/secretsmanager/utils/util.go deleted file mode 100644 index 8829b844..00000000 --- a/stackit/internal/services/secretsmanager/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *secretsmanager.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.SecretsManagerCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.SecretsManagerCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := secretsmanager.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/secretsmanager/utils/util_test.go b/stackit/internal/services/secretsmanager/utils/util_test.go deleted file mode 100644 index f2562a2d..00000000 --- a/stackit/internal/services/secretsmanager/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://secretsmanager-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *secretsmanager.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *secretsmanager.APIClient { - apiClient, err := secretsmanager.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - SecretsManagerCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *secretsmanager.APIClient { - apiClient, err := secretsmanager.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/serverbackup/schedule/resource.go b/stackit/internal/services/serverbackup/schedule/resource.go deleted file mode 100644 index ac69087e..00000000 --- a/stackit/internal/services/serverbackup/schedule/resource.go +++ /dev/null @@ -1,627 +0,0 @@ -package schedule - -import ( - "context" - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" - serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &scheduleResource{} - _ resource.ResourceWithConfigure = &scheduleResource{} - _ resource.ResourceWithImportState = &scheduleResource{} - _ resource.ResourceWithModifyPlan = &scheduleResource{} -) - -type Model struct { - ID types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - ServerId types.String `tfsdk:"server_id"` - BackupScheduleId types.Int64 `tfsdk:"backup_schedule_id"` - Name types.String `tfsdk:"name"` - Rrule types.String `tfsdk:"rrule"` - Enabled types.Bool `tfsdk:"enabled"` - BackupProperties *scheduleBackupPropertiesModel `tfsdk:"backup_properties"` - Region types.String `tfsdk:"region"` -} - -// scheduleBackupPropertiesModel maps schedule backup_properties data -type scheduleBackupPropertiesModel struct { - BackupName types.String `tfsdk:"name"` - RetentionPeriod types.Int64 `tfsdk:"retention_period"` - VolumeIds types.List `tfsdk:"volume_ids"` -} - -// NewScheduleResource is a helper function to simplify the provider implementation. -func NewScheduleResource() resource.Resource { - return &scheduleResource{} -} - -// scheduleResource is the resource implementation. -type scheduleResource struct { - client *serverbackup.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *scheduleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *scheduleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_backup_schedule" -} - -// Configure adds the provider configured client to the resource. -func (r *scheduleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_backup_schedule", "resource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := serverbackupUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Server backup client configured.") -} - -// Schema defines the schema for the resource. -func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Server backup schedule resource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server backup schedule resource schema. Must have a `region` specified in the provider configuration.", core.Resource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`backup_schedule_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "name": schema.StringAttribute{ - Description: "The schedule name.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - }, - }, - "backup_schedule_id": schema.Int64Attribute{ - Description: "Backup schedule ID.", - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID to which the server is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "Server ID for the backup schedule.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "rrule": schema.StringAttribute{ - Description: "Backup schedule described in `rrule` (recurrence rule) format.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.Rrule(), - validate.NoSeparator(), - }, - }, - "enabled": schema.BoolAttribute{ - Description: "Is the backup schedule enabled or disabled.", - Required: true, - }, - "backup_properties": schema.SingleNestedAttribute{ - Description: "Backup schedule details for the backups.", - Required: true, - Attributes: map[string]schema.Attribute{ - "volume_ids": schema.ListAttribute{ - ElementType: types.StringType, - Optional: true, - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - }, - "name": schema.StringAttribute{ - Required: true, - }, - "retention_period": schema.Int64Attribute{ - Required: true, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - }, - }, - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *scheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "region", region) - - // Enable backups if not already enabled - err := r.enableBackupsService(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server backup schedule", fmt.Sprintf("Enabling server backup project before creation: %v", err)) - return - } - - // Create new schedule - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server backup schedule", fmt.Sprintf("Creating API payload: %v", err)) - return - } - scheduleResp, err := r.client.CreateBackupSchedule(ctx, projectId, serverId, region).CreateBackupSchedulePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server backup schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - ctx = tflog.SetField(ctx, "backup_schedule_id", *scheduleResp.Id) - - // Map response body to schema - err = mapFields(ctx, scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server backup schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server backup schedule created.") -} - -// Read refreshes the Terraform state with the latest data. -func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - backupScheduleId := model.BackupScheduleId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "backup_schedule_id", backupScheduleId) - ctx = tflog.SetField(ctx, "region", region) - - scheduleResp, err := r.client.GetBackupSchedule(ctx, projectId, serverId, region, strconv.FormatInt(backupScheduleId, 10)).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading backup schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading backup schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server backup schedule read.") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - backupScheduleId := model.BackupScheduleId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "backup_schedule_id", backupScheduleId) - ctx = tflog.SetField(ctx, "region", region) - - // Update schedule - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server backup schedule", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - scheduleResp, err := r.client.UpdateBackupSchedule(ctx, projectId, serverId, region, strconv.FormatInt(backupScheduleId, 10)).UpdateBackupSchedulePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server backup schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server backup schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server backup schedule updated.") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - backupScheduleId := model.BackupScheduleId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "backup_schedule_id", backupScheduleId) - ctx = tflog.SetField(ctx, "region", region) - - err := r.client.DeleteBackupSchedule(ctx, projectId, serverId, region, strconv.FormatInt(backupScheduleId, 10)).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server backup schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Server backup schedule deleted.") - - // Disable backups service in case there are no backups and no backup schedules. - err = r.disableBackupsService(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server backup schedule", fmt.Sprintf("Disabling server backup service after deleting schedule: %v", err)) - return - } -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: // project_id,server_id,schedule_id -func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing server backup schedule", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[server_id],[backup_schedule_id], got %q", req.ID), - ) - return - } - - intId, err := strconv.ParseInt(idParts[3], 10, 64) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing server backup schedule", - fmt.Sprintf("Expected backup_schedule_id to be int64, got %q", idParts[2]), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("backup_schedule_id"), intId)...) - tflog.Info(ctx, "Server backup schedule state imported.") -} - -func mapFields(ctx context.Context, schedule *serverbackup.BackupSchedule, model *Model, region string) error { - if schedule == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - if schedule.Id == nil { - return fmt.Errorf("response id is nil") - } - - model.BackupScheduleId = types.Int64PointerValue(schedule.Id) - model.ID = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.ServerId.ValueString(), - strconv.FormatInt(model.BackupScheduleId.ValueInt64(), 10), - ) - model.Name = types.StringPointerValue(schedule.Name) - model.Rrule = types.StringPointerValue(schedule.Rrule) - model.Enabled = types.BoolPointerValue(schedule.Enabled) - if schedule.BackupProperties == nil { - model.BackupProperties = nil - return nil - } - - volIds := types.ListNull(types.StringType) - if schedule.BackupProperties.VolumeIds != nil { - var modelVolIds []string - if model.BackupProperties != nil { - var err error - modelVolIds, err = utils.ListValuetoStringSlice(model.BackupProperties.VolumeIds) - if err != nil { - return err - } - } - - respVolIds := *schedule.BackupProperties.VolumeIds - reconciledVolIds := utils.ReconcileStringSlices(modelVolIds, respVolIds) - - var diags diag.Diagnostics - volIds, diags = types.ListValueFrom(ctx, types.StringType, reconciledVolIds) - if diags.HasError() { - return fmt.Errorf("failed to map volumeIds: %w", core.DiagsToError(diags)) - } - } - model.BackupProperties = &scheduleBackupPropertiesModel{ - BackupName: types.StringValue(*schedule.BackupProperties.Name), - RetentionPeriod: types.Int64Value(*schedule.BackupProperties.RetentionPeriod), - VolumeIds: volIds, - } - model.Region = types.StringValue(region) - return nil -} - -// If already enabled, just continues -func (r *scheduleResource) enableBackupsService(ctx context.Context, model *Model) error { - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - tflog.Debug(ctx, "Enabling server backup service") - request := r.client.EnableServiceResource(ctx, projectId, serverId, region). - EnableServiceResourcePayload(serverbackup.EnableServiceResourcePayload{}) - - if err := request.Execute(); err != nil { - if strings.Contains(err.Error(), "Tried to activate already active service") { - tflog.Debug(ctx, "Service for server backup already enabled") - return nil - } - return fmt.Errorf("enable server backup service: %w", err) - } - tflog.Info(ctx, "Enabled server backup service") - return nil -} - -// Disables only if no backup schedules are present and no backups are present -func (r *scheduleResource) disableBackupsService(ctx context.Context, model *Model) error { - tflog.Debug(ctx, "Disabling server backup service (in case there are no backups and no backup schedules)") - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - tflog.Debug(ctx, "Checking for existing backups") - backups, err := r.client.ListBackups(ctx, projectId, serverId, region).Execute() - if err != nil { - return fmt.Errorf("list backups: %w", err) - } - if *backups.Items != nil && len(*backups.Items) > 0 { - tflog.Debug(ctx, "Backups found - will not disable server backup service") - return nil - } - - err = r.client.DisableServiceResourceExecute(ctx, projectId, serverId, region) - if err != nil { - return fmt.Errorf("disable server backup service: %w", err) - } - tflog.Info(ctx, "Disabled server backup service") - return nil -} - -func toCreatePayload(model *Model) (*serverbackup.CreateBackupSchedulePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - backupProperties := serverbackup.BackupProperties{} - if model.BackupProperties != nil { - ids := []string{} - var err error - if !(model.BackupProperties.VolumeIds.IsNull() || model.BackupProperties.VolumeIds.IsUnknown()) { - ids, err = utils.ListValuetoStringSlice(model.BackupProperties.VolumeIds) - if err != nil { - return nil, fmt.Errorf("convert volume id: %w", err) - } - } - // we should provide null to the API in case no volumeIds were chosen, else it errors - if len(ids) == 0 { - ids = nil - } - backupProperties = serverbackup.BackupProperties{ - Name: conversion.StringValueToPointer(model.BackupProperties.BackupName), - RetentionPeriod: conversion.Int64ValueToPointer(model.BackupProperties.RetentionPeriod), - VolumeIds: &ids, - } - } - return &serverbackup.CreateBackupSchedulePayload{ - Enabled: conversion.BoolValueToPointer(model.Enabled), - Name: conversion.StringValueToPointer(model.Name), - Rrule: conversion.StringValueToPointer(model.Rrule), - BackupProperties: &backupProperties, - }, nil -} - -func toUpdatePayload(model *Model) (*serverbackup.UpdateBackupSchedulePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - backupProperties := serverbackup.BackupProperties{} - if model.BackupProperties != nil { - ids := []string{} - var err error - if !(model.BackupProperties.VolumeIds.IsNull() || model.BackupProperties.VolumeIds.IsUnknown()) { - ids, err = utils.ListValuetoStringSlice(model.BackupProperties.VolumeIds) - if err != nil { - return nil, fmt.Errorf("convert volume id: %w", err) - } - } - // we should provide null to the API in case no volumeIds were chosen, else it errors - if len(ids) == 0 { - ids = nil - } - backupProperties = serverbackup.BackupProperties{ - Name: conversion.StringValueToPointer(model.BackupProperties.BackupName), - RetentionPeriod: conversion.Int64ValueToPointer(model.BackupProperties.RetentionPeriod), - VolumeIds: &ids, - } - } - - return &serverbackup.UpdateBackupSchedulePayload{ - Enabled: conversion.BoolValueToPointer(model.Enabled), - Name: conversion.StringValueToPointer(model.Name), - Rrule: conversion.StringValueToPointer(model.Rrule), - BackupProperties: &backupProperties, - }, nil -} diff --git a/stackit/internal/services/serverbackup/schedule/resource_test.go b/stackit/internal/services/serverbackup/schedule/resource_test.go deleted file mode 100644 index c3ecd45f..00000000 --- a/stackit/internal/services/serverbackup/schedule/resource_test.go +++ /dev/null @@ -1,238 +0,0 @@ -package schedule - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - sdk "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" -) - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *sdk.BackupSchedule - expected Model - isValid bool - }{ - { - "default_values", - &sdk.BackupSchedule{ - Id: utils.Ptr(int64(5)), - }, - Model{ - ID: types.StringValue("project_uid,eu01,server_uid,5"), - ProjectId: types.StringValue("project_uid"), - ServerId: types.StringValue("server_uid"), - BackupScheduleId: types.Int64Value(5), - }, - true, - }, - { - "simple_values", - &sdk.BackupSchedule{ - Id: utils.Ptr(int64(5)), - Enabled: utils.Ptr(true), - Name: utils.Ptr("backup_schedule_name_1"), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - BackupProperties: &sdk.BackupProperties{ - Name: utils.Ptr("backup_name_1"), - RetentionPeriod: utils.Ptr(int64(3)), - VolumeIds: &[]string{"uuid1", "uuid2"}, - }, - }, - Model{ - ServerId: types.StringValue("server_uid"), - ProjectId: types.StringValue("project_uid"), - BackupScheduleId: types.Int64Value(5), - ID: types.StringValue("project_uid,eu01,server_uid,5"), - Name: types.StringValue("backup_schedule_name_1"), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - Enabled: types.BoolValue(true), - BackupProperties: &scheduleBackupPropertiesModel{ - BackupName: types.StringValue("backup_name_1"), - RetentionPeriod: types.Int64Value(3), - VolumeIds: listValueFrom([]string{"uuid1", "uuid2"}), - }, - Region: types.StringValue("eu01"), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "no_resource_id", - &sdk.BackupSchedule{}, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - ServerId: tt.expected.ServerId, - } - ctx := context.TODO() - err := mapFields(ctx, tt.input, state, "eu01") - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *sdk.CreateBackupSchedulePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &sdk.CreateBackupSchedulePayload{ - BackupProperties: &sdk.BackupProperties{}, - }, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - Enabled: types.BoolValue(true), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - BackupProperties: nil, - }, - &sdk.CreateBackupSchedulePayload{ - Name: utils.Ptr("name"), - Enabled: utils.Ptr(true), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - BackupProperties: &sdk.BackupProperties{}, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - Rrule: types.StringValue(""), - }, - &sdk.CreateBackupSchedulePayload{ - BackupProperties: &sdk.BackupProperties{}, - Name: utils.Ptr(""), - Rrule: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *sdk.UpdateBackupSchedulePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &sdk.UpdateBackupSchedulePayload{ - BackupProperties: &sdk.BackupProperties{}, - }, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - Enabled: types.BoolValue(true), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - BackupProperties: nil, - }, - &sdk.UpdateBackupSchedulePayload{ - Name: utils.Ptr("name"), - Enabled: utils.Ptr(true), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - BackupProperties: &sdk.BackupProperties{}, - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - Rrule: types.StringValue(""), - }, - &sdk.UpdateBackupSchedulePayload{ - BackupProperties: &sdk.BackupProperties{}, - Name: utils.Ptr(""), - Rrule: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/serverbackup/schedule/schedule_datasource.go b/stackit/internal/services/serverbackup/schedule/schedule_datasource.go deleted file mode 100644 index 89691faf..00000000 --- a/stackit/internal/services/serverbackup/schedule/schedule_datasource.go +++ /dev/null @@ -1,196 +0,0 @@ -package schedule - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" -) - -// scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. -// This is a workaround for the lack of a global state in the provider and -// needs to exist because the Configure method is called twice. -var scheduleDataSourceBetaCheckDone bool - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &scheduleDataSource{} -) - -// NewScheduleDataSource is a helper function to simplify the provider implementation. -func NewScheduleDataSource() datasource.DataSource { - return &scheduleDataSource{} -} - -// scheduleDataSource is the data source implementation. -type scheduleDataSource struct { - client *serverbackup.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *scheduleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_backup_schedule" -} - -// Configure adds the provider configured client to the data source. -func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - if !scheduleDataSourceBetaCheckDone { - features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_backup_schedule", "data source") - if resp.Diagnostics.HasError() { - return - } - scheduleDataSourceBetaCheckDone = true - } - - apiClient := serverbackupUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Server backup client configured") -} - -// Schema defines the schema for the data source. -func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Server backup schedule datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server backup schedule datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`backup_schedule_id`\".", - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "The schedule name.", - Computed: true, - }, - "backup_schedule_id": schema.Int64Attribute{ - Description: "Backup schedule ID.", - Required: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID to which the server is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "Server ID for the backup schedule.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "rrule": schema.StringAttribute{ - Description: "Backup schedule described in `rrule` (recurrence rule) format.", - Computed: true, - }, - "enabled": schema.BoolAttribute{ - Description: "Is the backup schedule enabled or disabled.", - Computed: true, - }, - "backup_properties": schema.SingleNestedAttribute{ - Description: "Backup schedule details for the backups.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "volume_ids": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "name": schema.StringAttribute{ - Computed: true, - }, - "retention_period": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - backupScheduleId := model.BackupScheduleId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "backup_schedule_id", backupScheduleId) - ctx = tflog.SetField(ctx, "region", region) - - scheduleResp, err := r.client.GetBackupSchedule(ctx, projectId, serverId, region, strconv.FormatInt(backupScheduleId, 10)).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading server backup schedule", - fmt.Sprintf("Backup schedule with ID %q or server with ID %q does not exist in project %q.", strconv.FormatInt(backupScheduleId, 10), serverId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server backup schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server backup schedule read") -} diff --git a/stackit/internal/services/serverbackup/schedule/schedules_datasource.go b/stackit/internal/services/serverbackup/schedule/schedules_datasource.go deleted file mode 100644 index 23f0a2e8..00000000 --- a/stackit/internal/services/serverbackup/schedule/schedules_datasource.go +++ /dev/null @@ -1,252 +0,0 @@ -package schedule - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" -) - -// scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. -// This is a workaround for the lack of a global state in the provider and -// needs to exist because the Configure method is called twice. -var schedulesDataSourceBetaCheckDone bool - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &schedulesDataSource{} -) - -// NewSchedulesDataSource is a helper function to simplify the provider implementation. -func NewSchedulesDataSource() datasource.DataSource { - return &schedulesDataSource{} -} - -// schedulesDataSource is the data source implementation. -type schedulesDataSource struct { - client *serverbackup.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *schedulesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_backup_schedules" -} - -// Configure adds the provider configured client to the data source. -func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - if !schedulesDataSourceBetaCheckDone { - features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_backup_schedules", "data source") - if resp.Diagnostics.HasError() { - return - } - schedulesDataSourceBetaCheckDone = true - } - - apiClient := serverbackupUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - - r.client = apiClient - tflog.Info(ctx, "Server backup client configured") -} - -// Schema defines the schema for the data source. -func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Server backup schedules datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server backup schedules datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`server_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID (UUID) to which the server is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "Server ID (UUID) to which the backup schedule is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "items": schema.ListNestedAttribute{ - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "backup_schedule_id": schema.Int64Attribute{ - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "The backup schedule name.", - Computed: true, - }, - "rrule": schema.StringAttribute{ - Description: "Backup schedule described in `rrule` (recurrence rule) format.", - Computed: true, - }, - "enabled": schema.BoolAttribute{ - Description: "Is the backup schedule enabled or disabled.", - Computed: true, - }, - "backup_properties": schema.SingleNestedAttribute{ - Description: "Backup schedule details for the backups.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "volume_ids": schema.ListAttribute{ - ElementType: types.StringType, - Computed: true, - }, - "name": schema.StringAttribute{ - Computed: true, - }, - "retention_period": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - }, - }, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - }, - } -} - -// schedulesDataSourceModel maps the data source schema data. -type schedulesDataSourceModel struct { - ID types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - ServerId types.String `tfsdk:"server_id"` - Items []schedulesDatasourceItemModel `tfsdk:"items"` - Region types.String `tfsdk:"region"` -} - -// schedulesDatasourceItemModel maps schedule schema data. -type schedulesDatasourceItemModel struct { - BackupScheduleId types.Int64 `tfsdk:"backup_schedule_id"` - Name types.String `tfsdk:"name"` - Rrule types.String `tfsdk:"rrule"` - Enabled types.Bool `tfsdk:"enabled"` - BackupProperties *scheduleBackupPropertiesModel `tfsdk:"backup_properties"` -} - -// Read refreshes the Terraform state with the latest data. -func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model schedulesDataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "region", region) - - schedules, err := r.client.ListBackupSchedules(ctx, projectId, serverId, region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading server backup schedules", - fmt.Sprintf("Server with ID %q does not exist in project %q.", serverId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapSchedulesDatasourceFields(ctx, schedules, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server backup schedules", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server backup schedules read") -} - -func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverbackup.GetBackupSchedulesResponse, model *schedulesDataSourceModel, region string) error { - if schedules == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - tflog.Debug(ctx, "response", map[string]any{"schedules": schedules}) - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - - model.ID = utils.BuildInternalTerraformId(projectId, region, serverId) - model.Region = types.StringValue(region) - - for _, schedule := range *schedules.Items { - scheduleState := schedulesDatasourceItemModel{ - BackupScheduleId: types.Int64Value(*schedule.Id), - Name: types.StringValue(*schedule.Name), - Rrule: types.StringValue(*schedule.Rrule), - Enabled: types.BoolValue(*schedule.Enabled), - } - ids, diags := types.ListValueFrom(ctx, types.StringType, schedule.BackupProperties.VolumeIds) - if diags.HasError() { - return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) - } - scheduleState.BackupProperties = &scheduleBackupPropertiesModel{ - BackupName: types.StringValue(*schedule.BackupProperties.Name), - RetentionPeriod: types.Int64Value(*schedule.BackupProperties.RetentionPeriod), - VolumeIds: ids, - } - model.Items = append(model.Items, scheduleState) - } - return nil -} diff --git a/stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go b/stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go deleted file mode 100644 index 70bb3d26..00000000 --- a/stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package schedule - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - sdk "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" -) - -func listValueFrom(items []string) basetypes.ListValue { - val, _ := types.ListValueFrom(context.TODO(), types.StringType, items) - return val -} - -func TestMapSchedulesDataSourceFields(t *testing.T) { - tests := []struct { - description string - input *sdk.GetBackupSchedulesResponse - expected schedulesDataSourceModel - isValid bool - }{ - { - "empty response", - &sdk.GetBackupSchedulesResponse{ - Items: &[]sdk.BackupSchedule{}, - }, - schedulesDataSourceModel{ - ID: types.StringValue("project_uid,eu01,server_uid"), - ProjectId: types.StringValue("project_uid"), - ServerId: types.StringValue("server_uid"), - Items: nil, - Region: types.StringValue("eu01"), - }, - true, - }, - { - "simple_values", - &sdk.GetBackupSchedulesResponse{ - Items: &[]sdk.BackupSchedule{ - { - Id: utils.Ptr(int64(5)), - Enabled: utils.Ptr(true), - Name: utils.Ptr("backup_schedule_name_1"), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - BackupProperties: &sdk.BackupProperties{ - Name: utils.Ptr("backup_name_1"), - RetentionPeriod: utils.Ptr(int64(14)), - VolumeIds: &[]string{"uuid1", "uuid2"}, - }, - }, - }, - }, - schedulesDataSourceModel{ - ID: types.StringValue("project_uid,eu01,server_uid"), - ServerId: types.StringValue("server_uid"), - ProjectId: types.StringValue("project_uid"), - Items: []schedulesDatasourceItemModel{ - { - BackupScheduleId: types.Int64Value(5), - Name: types.StringValue("backup_schedule_name_1"), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - Enabled: types.BoolValue(true), - BackupProperties: &scheduleBackupPropertiesModel{ - BackupName: types.StringValue("backup_name_1"), - RetentionPeriod: types.Int64Value(14), - VolumeIds: listValueFrom([]string{"uuid1", "uuid2"}), - }, - }, - }, - Region: types.StringValue("eu01"), - }, - true, - }, - { - "nil_response", - nil, - schedulesDataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &schedulesDataSourceModel{ - ProjectId: tt.expected.ProjectId, - ServerId: tt.expected.ServerId, - } - ctx := context.TODO() - err := mapSchedulesDatasourceFields(ctx, tt.input, state, "eu01") - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/serverbackup/serverbackup_acc_test.go b/stackit/internal/services/serverbackup/serverbackup_acc_test.go deleted file mode 100644 index 9793291b..00000000 --- a/stackit/internal/services/serverbackup/serverbackup_acc_test.go +++ /dev/null @@ -1,302 +0,0 @@ -package serverbackup_test - -import ( - "context" - _ "embed" - "fmt" - "maps" - "regexp" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - core_config "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/resource-min.tf - resourceMinConfig string - - //go:embed testdata/resource-max.tf - resourceMaxConfig string -) - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "server_id": config.StringVariable(testutil.ServerId), - "schedule_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "rrule": config.StringVariable("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - "enabled": config.BoolVariable(true), - "backup_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "retention_period": config.IntegerVariable(14), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "server_id": config.StringVariable(testutil.ServerId), - "schedule_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "rrule": config.StringVariable("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - "enabled": config.BoolVariable(true), - "backup_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "retention_period": config.IntegerVariable(14), - "region": config.StringVariable("eu01"), -} - -func configVarsInvalid(vars config.Variables) config.Variables { - tempConfig := maps.Clone(vars) - tempConfig["retention_period"] = config.IntegerVariable(0) - return tempConfig -} - -func configVarsMinUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMin) - tempConfig["retention_period"] = config.IntegerVariable(12) - tempConfig["rrule"] = config.StringVariable("DTSTART;TZID=Europe/Berlin:20250430T010000 RRULE:FREQ=DAILY;INTERVAL=3") - - return tempConfig -} - -func configVarsMaxUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMax) - tempConfig["retention_period"] = config.IntegerVariable(12) - tempConfig["rrule"] = config.StringVariable("DTSTART;TZID=Europe/Berlin:20250430T010000 RRULE:FREQ=DAILY;INTERVAL=3") - return tempConfig -} - -func TestAccServerBackupScheduleMinResource(t *testing.T) { - if testutil.ServerId == "" { - fmt.Println("TF_ACC_SERVER_ID not set, skipping test") - return - } - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckServerBackupScheduleDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: configVarsInvalid(testConfigVarsMin), - ExpectError: regexp.MustCompile(`.*backup_properties.retention_period value must be at least 1*`), - }, - // Creation - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Backup schedule data - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "server_id", testutil.ConvertConfigVariable(testConfigVarsMin["server_id"])), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "backup_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "name", testutil.ConvertConfigVariable(testConfigVarsMin["schedule_name"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(testConfigVarsMin["rrule"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "enabled", strconv.FormatBool(true)), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "backup_properties.name", testutil.ConvertConfigVariable(testConfigVarsMin["backup_name"])), - ), - }, - // data source - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Server backup schedule data - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "server_id", testutil.ConvertConfigVariable(testConfigVarsMin["server_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_backup_schedule.schedule_data_test", "backup_schedule_id"), - resource.TestCheckResourceAttrSet("data.stackit_server_backup_schedule.schedule_data_test", "id"), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "name", testutil.ConvertConfigVariable(testConfigVarsMin["schedule_name"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "rrule", testutil.ConvertConfigVariable(testConfigVarsMin["rrule"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "enabled", strconv.FormatBool(true)), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "backup_properties.name", testutil.ConvertConfigVariable(testConfigVarsMin["backup_name"])), - - // Server backup schedules data - resource.TestCheckResourceAttr("data.stackit_server_backup_schedules.schedules_data_test", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedules.schedules_data_test", "server_id", testutil.ConvertConfigVariable(testConfigVarsMin["server_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_backup_schedules.schedules_data_test", "id"), - ), - }, - // Import - { - ResourceName: "stackit_server_backup_schedule.test_schedule", - ConfigVariables: testConfigVarsMin, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server_backup_schedule.test_schedule"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server_backup_schedule.test_schedule") - } - scheduleId, ok := r.Primary.Attributes["backup_schedule_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute backup_schedule_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: configVarsMinUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Backup schedule data - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "server_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["server_id"])), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "backup_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "name", testutil.ConvertConfigVariable(configVarsMinUpdated()["schedule_name"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(configVarsMinUpdated()["rrule"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(configVarsMinUpdated()["enabled"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "backup_properties.retention_period", testutil.ConvertConfigVariable(configVarsMinUpdated()["retention_period"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "backup_properties.name", testutil.ConvertConfigVariable(configVarsMinUpdated()["backup_name"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccServerBackupScheduleMaxResource(t *testing.T) { - if testutil.ServerId == "" { - fmt.Println("TF_ACC_SERVER_ID not set, skipping test") - return - } - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckServerBackupScheduleDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: configVarsInvalid(testConfigVarsMax), - ExpectError: regexp.MustCompile(`.*backup_properties.retention_period value must be at least 1*`), - }, - // Creation - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Backup schedule data - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "server_id", testutil.ConvertConfigVariable(testConfigVarsMax["server_id"])), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "backup_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "name", testutil.ConvertConfigVariable(testConfigVarsMax["schedule_name"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(testConfigVarsMax["rrule"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "enabled", strconv.FormatBool(true)), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "backup_properties.name", testutil.ConvertConfigVariable(testConfigVarsMax["backup_name"])), - ), - }, - // data source - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Server backup schedule data - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "server_id", testutil.ConvertConfigVariable(testConfigVarsMax["server_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_backup_schedule.schedule_data_test", "backup_schedule_id"), - resource.TestCheckResourceAttrSet("data.stackit_server_backup_schedule.schedule_data_test", "id"), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "name", testutil.ConvertConfigVariable(testConfigVarsMax["schedule_name"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "rrule", testutil.ConvertConfigVariable(testConfigVarsMax["rrule"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "enabled", strconv.FormatBool(true)), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedule.schedule_data_test", "backup_properties.name", testutil.ConvertConfigVariable(testConfigVarsMax["backup_name"])), - - // Server backup schedules data - resource.TestCheckResourceAttr("data.stackit_server_backup_schedules.schedules_data_test", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_server_backup_schedules.schedules_data_test", "server_id", testutil.ConvertConfigVariable(testConfigVarsMax["server_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_backup_schedules.schedules_data_test", "id"), - ), - }, - // Import - { - ResourceName: "stackit_server_backup_schedule.test_schedule", - ConfigVariables: testConfigVarsMax, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server_backup_schedule.test_schedule"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server_backup_schedule.test_schedule") - } - scheduleId, ok := r.Primary.Attributes["backup_schedule_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute backup_schedule_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - Config: testutil.ServerBackupProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: configVarsMaxUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Backup schedule data - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "server_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["server_id"])), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "backup_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_backup_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["schedule_name"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(configVarsMaxUpdated()["rrule"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["enabled"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "backup_properties.retention_period", testutil.ConvertConfigVariable(configVarsMaxUpdated()["retention_period"])), - resource.TestCheckResourceAttr("stackit_server_backup_schedule.test_schedule", "backup_properties.name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["backup_name"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckServerBackupScheduleDestroy(s *terraform.State) error { - ctx := context.Background() - var client *serverbackup.APIClient - var err error - if testutil.ServerBackupCustomEndpoint == "" { - client, err = serverbackup.NewAPIClient() - } else { - client, err = serverbackup.NewAPIClient( - core_config.WithEndpoint(testutil.ServerBackupCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - schedulesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_server_backup_schedule" { - continue - } - // server backup schedule terraform ID: "[project_id],[server_id],[backup_schedule_id]" - scheduleId := strings.Split(rs.Primary.ID, core.Separator)[3] - schedulesToDestroy = append(schedulesToDestroy, scheduleId) - } - - schedulesResp, err := client.ListBackupSchedules(ctx, testutil.ProjectId, testutil.ServerId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting schedulesResp: %w", err) - } - - schedules := *schedulesResp.Items - for i := range schedules { - if schedules[i].Id == nil { - continue - } - scheduleId := strconv.FormatInt(*schedules[i].Id, 10) - if utils.Contains(schedulesToDestroy, scheduleId) { - err := client.DeleteBackupScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId, testutil.Region) - if err != nil { - return fmt.Errorf("destroying server backup schedule %s during CheckDestroy: %w", scheduleId, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/serverbackup/testdata/resource-max.tf b/stackit/internal/services/serverbackup/testdata/resource-max.tf deleted file mode 100644 index 802b73b7..00000000 --- a/stackit/internal/services/serverbackup/testdata/resource-max.tf +++ /dev/null @@ -1,34 +0,0 @@ -variable "project_id" {} -variable "server_id" {} -variable "schedule_name" {} -variable "rrule" {} -variable "enabled" {} -variable "backup_name" {} -variable "retention_period" {} -variable "region" {} - - -resource "stackit_server_backup_schedule" "test_schedule" { - project_id = var.project_id - server_id = var.server_id - name = var.schedule_name - rrule = var.rrule - enabled = var.enabled - backup_properties = { - name = var.backup_name - retention_period = var.retention_period - volume_ids = null - } - region = var.region -} - -data "stackit_server_backup_schedule" "schedule_data_test" { - project_id = var.project_id - server_id = var.server_id - backup_schedule_id = stackit_server_backup_schedule.test_schedule.backup_schedule_id -} - -data "stackit_server_backup_schedules" "schedules_data_test" { - project_id = var.project_id - server_id = var.server_id -} diff --git a/stackit/internal/services/serverbackup/testdata/resource-min.tf b/stackit/internal/services/serverbackup/testdata/resource-min.tf deleted file mode 100644 index 5cdf3037..00000000 --- a/stackit/internal/services/serverbackup/testdata/resource-min.tf +++ /dev/null @@ -1,32 +0,0 @@ -variable "project_id" {} -variable "server_id" {} -variable "schedule_name" {} -variable "rrule" {} -variable "enabled" {} -variable "backup_name" {} -variable "retention_period" {} - - -resource "stackit_server_backup_schedule" "test_schedule" { - project_id = var.project_id - server_id = var.server_id - name = var.schedule_name - rrule = var.rrule - enabled = var.enabled - backup_properties = { - name = var.backup_name - retention_period = var.retention_period - volume_ids = null - } -} - -data "stackit_server_backup_schedule" "schedule_data_test" { - project_id = var.project_id - server_id = var.server_id - backup_schedule_id = stackit_server_backup_schedule.test_schedule.backup_schedule_id -} - -data "stackit_server_backup_schedules" "schedules_data_test" { - project_id = var.project_id - server_id = var.server_id -} diff --git a/stackit/internal/services/serverbackup/utils/util.go b/stackit/internal/services/serverbackup/utils/util.go deleted file mode 100644 index 1869bd5e..00000000 --- a/stackit/internal/services/serverbackup/utils/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *serverbackup.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ServerBackupCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ServerBackupCustomEndpoint)) - } - apiClient, err := serverbackup.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/serverbackup/utils/util_test.go b/stackit/internal/services/serverbackup/utils/util_test.go deleted file mode 100644 index e282c954..00000000 --- a/stackit/internal/services/serverbackup/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://serverbackup-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *serverbackup.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *serverbackup.APIClient { - apiClient, err := serverbackup.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ServerBackupCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *serverbackup.APIClient { - apiClient, err := serverbackup.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/serverupdate/schedule/resource.go b/stackit/internal/services/serverupdate/schedule/resource.go deleted file mode 100644 index c6a20d36..00000000 --- a/stackit/internal/services/serverupdate/schedule/resource.go +++ /dev/null @@ -1,495 +0,0 @@ -package schedule - -import ( - "context" - "fmt" - "net/http" - "strconv" - "strings" - - serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &scheduleResource{} - _ resource.ResourceWithConfigure = &scheduleResource{} - _ resource.ResourceWithImportState = &scheduleResource{} - _ resource.ResourceWithModifyPlan = &scheduleResource{} -) - -type Model struct { - ID types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - ServerId types.String `tfsdk:"server_id"` - UpdateScheduleId types.Int64 `tfsdk:"update_schedule_id"` - Name types.String `tfsdk:"name"` - Rrule types.String `tfsdk:"rrule"` - Enabled types.Bool `tfsdk:"enabled"` - MaintenanceWindow types.Int64 `tfsdk:"maintenance_window"` - Region types.String `tfsdk:"region"` -} - -// NewScheduleResource is a helper function to simplify the provider implementation. -func NewScheduleResource() resource.Resource { - return &scheduleResource{} -} - -// scheduleResource is the resource implementation. -type scheduleResource struct { - client *serverupdate.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *scheduleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *scheduleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_update_schedule" -} - -// Configure adds the provider configured client to the resource. -func (r *scheduleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "resource") - if resp.Diagnostics.HasError() { - return - } - - apiClient := serverupdateUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Server update client configured.") -} - -// Schema defines the schema for the resource. -func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Server update schedule resource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server update schedule resource schema. Must have a `region` specified in the provider configuration.", core.Resource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "name": schema.StringAttribute{ - Description: "The schedule name.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - }, - }, - "update_schedule_id": schema.Int64Attribute{ - Description: "Update schedule ID.", - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID to which the server is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "Server ID for the update schedule.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "rrule": schema.StringAttribute{ - Description: "Update schedule described in `rrule` (recurrence rule) format.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.Rrule(), - validate.NoSeparator(), - }, - }, - "enabled": schema.BoolAttribute{ - Description: "Is the update schedule enabled or disabled.", - Required: true, - }, - "maintenance_window": schema.Int64Attribute{ - Description: "Maintenance window [1..24].", - Required: true, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - int64validator.AtMost(24), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: "The resource region. If not defined, the provider region is used.", - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *scheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "region", region) - - // Enable updates if not already enabled - err := enableUpdatesService(ctx, &model, r.client, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Enabling server update project before creation: %v", err)) - return - } - - // Create new schedule - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Creating API payload: %v", err)) - return - } - scheduleResp, err := r.client.CreateUpdateSchedule(ctx, projectId, serverId, region).CreateUpdateSchedulePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - ctx = tflog.SetField(ctx, "update_schedule_id", *scheduleResp.Id) - - // Map response body to schema - err = mapFields(scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server update schedule created.") -} - -// Read refreshes the Terraform state with the latest data. -func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - updateScheduleId := model.UpdateScheduleId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) - - scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading update schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading update schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server update schedule read.") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - updateScheduleId := model.UpdateScheduleId.ValueInt64() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) - - // Update schedule - payload, err := toUpdatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - scheduleResp, err := r.client.UpdateUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).UpdateUpdateSchedulePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server update schedule updated.") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - updateScheduleId := model.UpdateScheduleId.ValueInt64() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "region", region) - ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) - - err := r.client.DeleteUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server update schedule", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Server update schedule deleted.") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: // project_id,server_id,schedule_id -func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing server update schedule", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[server_id],[update_schedule_id], got %q", req.ID), - ) - return - } - - intId, err := strconv.ParseInt(idParts[3], 10, 64) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing server update schedule", - fmt.Sprintf("Expected update_schedule_id to be int64, got %q", idParts[2]), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("update_schedule_id"), intId)...) - tflog.Info(ctx, "Server update schedule state imported.") -} - -func mapFields(schedule *serverupdate.UpdateSchedule, model *Model, region string) error { - if schedule == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - if schedule.Id == nil { - return fmt.Errorf("response id is nil") - } - - model.UpdateScheduleId = types.Int64PointerValue(schedule.Id) - model.ID = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.ServerId.ValueString(), - strconv.FormatInt(model.UpdateScheduleId.ValueInt64(), 10), - ) - model.Name = types.StringPointerValue(schedule.Name) - model.Rrule = types.StringPointerValue(schedule.Rrule) - model.Enabled = types.BoolPointerValue(schedule.Enabled) - model.MaintenanceWindow = types.Int64PointerValue(schedule.MaintenanceWindow) - model.Region = types.StringValue(region) - return nil -} - -// If already enabled, just continues -func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient, region string) error { - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - payload := serverupdate.EnableServiceResourcePayload{} - - tflog.Debug(ctx, "Enabling server update service") - err := client.EnableServiceResource(ctx, projectId, serverId, region).EnableServiceResourcePayload(payload).Execute() - if err != nil { - if strings.Contains(err.Error(), "Tried to activate already active service") { - tflog.Debug(ctx, "Service for server update already enabled") - return nil - } - return fmt.Errorf("enable server update service: %w", err) - } - tflog.Info(ctx, "Enabled server update service") - return nil -} - -func toCreatePayload(model *Model) (*serverupdate.CreateUpdateSchedulePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &serverupdate.CreateUpdateSchedulePayload{ - Enabled: conversion.BoolValueToPointer(model.Enabled), - Name: conversion.StringValueToPointer(model.Name), - Rrule: conversion.StringValueToPointer(model.Rrule), - MaintenanceWindow: conversion.Int64ValueToPointer(model.MaintenanceWindow), - }, nil -} - -func toUpdatePayload(model *Model) (*serverupdate.UpdateUpdateSchedulePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &serverupdate.UpdateUpdateSchedulePayload{ - Enabled: conversion.BoolValueToPointer(model.Enabled), - Name: conversion.StringValueToPointer(model.Name), - Rrule: conversion.StringValueToPointer(model.Rrule), - MaintenanceWindow: conversion.Int64ValueToPointer(model.MaintenanceWindow), - }, nil -} diff --git a/stackit/internal/services/serverupdate/schedule/resource_test.go b/stackit/internal/services/serverupdate/schedule/resource_test.go deleted file mode 100644 index 43cb2895..00000000 --- a/stackit/internal/services/serverupdate/schedule/resource_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package schedule - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - sdk "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" -) - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *sdk.UpdateSchedule - region string - expected Model - isValid bool - }{ - { - "default_values", - &sdk.UpdateSchedule{ - Id: utils.Ptr(int64(5)), - }, - testRegion, - Model{ - ID: types.StringValue("project_uid,region,server_uid,5"), - ProjectId: types.StringValue("project_uid"), - ServerId: types.StringValue("server_uid"), - UpdateScheduleId: types.Int64Value(5), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &sdk.UpdateSchedule{ - Id: utils.Ptr(int64(5)), - Enabled: utils.Ptr(true), - Name: utils.Ptr("update_schedule_name_1"), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: utils.Ptr(int64(1)), - }, - testRegion, - Model{ - ServerId: types.StringValue("server_uid"), - ProjectId: types.StringValue("project_uid"), - UpdateScheduleId: types.Int64Value(5), - ID: types.StringValue("project_uid,region,server_uid,5"), - Name: types.StringValue("update_schedule_name_1"), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - Enabled: types.BoolValue(true), - MaintenanceWindow: types.Int64Value(1), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &sdk.UpdateSchedule{}, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - ServerId: tt.expected.ServerId, - } - err := mapFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *sdk.CreateUpdateSchedulePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &sdk.CreateUpdateSchedulePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - Enabled: types.BoolValue(true), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: types.Int64Value(1), - }, - &sdk.CreateUpdateSchedulePayload{ - Name: utils.Ptr("name"), - Enabled: utils.Ptr(true), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: utils.Ptr(int64(1)), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - Rrule: types.StringValue(""), - }, - &sdk.CreateUpdateSchedulePayload{ - Name: utils.Ptr(""), - Rrule: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *sdk.UpdateUpdateSchedulePayload - isValid bool - }{ - { - "default_values", - &Model{}, - &sdk.UpdateUpdateSchedulePayload{}, - true, - }, - { - "simple_values", - &Model{ - Name: types.StringValue("name"), - Enabled: types.BoolValue(true), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: types.Int64Value(1), - }, - &sdk.UpdateUpdateSchedulePayload{ - Name: utils.Ptr("name"), - Enabled: utils.Ptr(true), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: utils.Ptr(int64(1)), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Name: types.StringValue(""), - Rrule: types.StringValue(""), - }, - &sdk.UpdateUpdateSchedulePayload{ - Name: utils.Ptr(""), - Rrule: utils.Ptr(""), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go b/stackit/internal/services/serverupdate/schedule/schedule_datasource.go deleted file mode 100644 index 83768164..00000000 --- a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go +++ /dev/null @@ -1,182 +0,0 @@ -package schedule - -import ( - "context" - "fmt" - "net/http" - "strconv" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" -) - -// scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. -// This is a workaround for the lack of a global state in the provider and -// needs to exist because the Configure method is called twice. -var scheduleDataSourceBetaCheckDone bool - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &scheduleDataSource{} -) - -// NewScheduleDataSource is a helper function to simplify the provider implementation. -func NewScheduleDataSource() datasource.DataSource { - return &scheduleDataSource{} -} - -// scheduleDataSource is the data source implementation. -type scheduleDataSource struct { - client *serverupdate.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *scheduleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_update_schedule" -} - -// Configure adds the provider configured client to the data source. -func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - if !scheduleDataSourceBetaCheckDone { - features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "data source") - if resp.Diagnostics.HasError() { - return - } - scheduleDataSourceBetaCheckDone = true - } - - apiClient := serverupdateUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Server update client configured") -} - -// Schema defines the schema for the data source. -func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Server update schedule datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server update schedule datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".", - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "The schedule name.", - Computed: true, - }, - "update_schedule_id": schema.Int64Attribute{ - Description: "Update schedule ID.", - Required: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID to which the server is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "Server ID for the update schedule.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "rrule": schema.StringAttribute{ - Description: "Update schedule described in `rrule` (recurrence rule) format.", - Computed: true, - }, - "enabled": schema.BoolAttribute{ - Description: "Is the update schedule enabled or disabled.", - Computed: true, - }, - "maintenance_window": schema.Int64Attribute{ - Description: "Maintenance window [1..24].", - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - updateScheduleId := model.UpdateScheduleId.ValueInt64() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - ctx = tflog.SetField(ctx, "update_schedule_id", updateScheduleId) - ctx = tflog.SetField(ctx, "region", region) - - scheduleResp, err := r.client.GetUpdateSchedule(ctx, projectId, serverId, strconv.FormatInt(updateScheduleId, 10), region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading server update schedule", - fmt.Sprintf("Update schedule with ID %q or server with ID %q does not exist in project %q.", strconv.FormatInt(updateScheduleId, 10), serverId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(scheduleResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update schedule", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server update schedule read") -} diff --git a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go b/stackit/internal/services/serverupdate/schedule/schedules_datasource.go deleted file mode 100644 index 117f2059..00000000 --- a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go +++ /dev/null @@ -1,230 +0,0 @@ -package schedule - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" -) - -// scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. -// This is a workaround for the lack of a global state in the provider and -// needs to exist because the Configure method is called twice. -var schedulesDataSourceBetaCheckDone bool - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &schedulesDataSource{} -) - -// NewSchedulesDataSource is a helper function to simplify the provider implementation. -func NewSchedulesDataSource() datasource.DataSource { - return &schedulesDataSource{} -} - -// schedulesDataSource is the data source implementation. -type schedulesDataSource struct { - client *serverupdate.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *schedulesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_server_update_schedules" -} - -// Configure adds the provider configured client to the data source. -func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - if !schedulesDataSourceBetaCheckDone { - features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedules", "data source") - if resp.Diagnostics.HasError() { - return - } - schedulesDataSourceBetaCheckDone = true - } - - apiClient := serverupdateUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Server update client configured") -} - -// Schema defines the schema for the data source. -func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "Server update schedules datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server update schedules datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`server_id`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT Project ID (UUID) to which the server is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "server_id": schema.StringAttribute{ - Description: "Server ID (UUID) to which the update schedule is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "items": schema.ListNestedAttribute{ - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "update_schedule_id": schema.Int64Attribute{ - Computed: true, - }, - "name": schema.StringAttribute{ - Description: "The update schedule name.", - Computed: true, - }, - "rrule": schema.StringAttribute{ - Description: "Update schedule described in `rrule` (recurrence rule) format.", - Computed: true, - }, - "enabled": schema.BoolAttribute{ - Description: "Is the update schedule enabled or disabled.", - Computed: true, - }, - "maintenance_window": schema.Int64Attribute{ - Description: "Maintenance window [1..24].", - Computed: true, - }, - }, - }, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - }, - } -} - -// schedulesDataSourceModel maps the data source schema data. -type schedulesDataSourceModel struct { - ID types.String `tfsdk:"id"` - ProjectId types.String `tfsdk:"project_id"` - ServerId types.String `tfsdk:"server_id"` - Items []schedulesDatasourceItemModel `tfsdk:"items"` - Region types.String `tfsdk:"region"` -} - -// schedulesDatasourceItemModel maps schedule schema data. -type schedulesDatasourceItemModel struct { - UpdateScheduleId types.Int64 `tfsdk:"update_schedule_id"` - Name types.String `tfsdk:"name"` - Rrule types.String `tfsdk:"rrule"` - Enabled types.Bool `tfsdk:"enabled"` - MaintenanceWindow types.Int64 `tfsdk:"maintenance_window"` -} - -// Read refreshes the Terraform state with the latest data. -func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model schedulesDataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "server_id", serverId) - - schedules, err := r.client.ListUpdateSchedules(ctx, projectId, serverId, region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading server update schedules", - fmt.Sprintf("Server with ID %q does not exist in project %q.", serverId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapSchedulesDatasourceFields(ctx, schedules, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server update schedules", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Server update schedules read") -} - -func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.GetUpdateSchedulesResponse, model *schedulesDataSourceModel, region string) error { - if schedules == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - tflog.Debug(ctx, "response", map[string]any{"schedules": schedules}) - projectId := model.ProjectId.ValueString() - serverId := model.ServerId.ValueString() - - model.ID = utils.BuildInternalTerraformId(projectId, region, serverId) - model.Region = types.StringValue(region) - - for _, schedule := range *schedules.Items { - scheduleState := schedulesDatasourceItemModel{ - UpdateScheduleId: types.Int64Value(*schedule.Id), - Name: types.StringValue(*schedule.Name), - Rrule: types.StringValue(*schedule.Rrule), - Enabled: types.BoolValue(*schedule.Enabled), - MaintenanceWindow: types.Int64Value(*schedule.MaintenanceWindow), - } - model.Items = append(model.Items, scheduleState) - } - return nil -} diff --git a/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go b/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go deleted file mode 100644 index 2619daf0..00000000 --- a/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package schedule - -import ( - "context" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - sdk "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" -) - -func TestMapSchedulesDataSourceFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *sdk.GetUpdateSchedulesResponse - region string - expected schedulesDataSourceModel - isValid bool - }{ - { - "empty response", - &sdk.GetUpdateSchedulesResponse{ - Items: &[]sdk.UpdateSchedule{}, - }, - testRegion, - schedulesDataSourceModel{ - ID: types.StringValue("project_uid,region,server_uid"), - ProjectId: types.StringValue("project_uid"), - ServerId: types.StringValue("server_uid"), - Items: nil, - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &sdk.GetUpdateSchedulesResponse{ - Items: &[]sdk.UpdateSchedule{ - { - Id: utils.Ptr(int64(5)), - Enabled: utils.Ptr(true), - Name: utils.Ptr("update_schedule_name_1"), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: utils.Ptr(int64(1)), - }, - }, - }, - testRegion, - schedulesDataSourceModel{ - ID: types.StringValue("project_uid,region,server_uid"), - ServerId: types.StringValue("server_uid"), - ProjectId: types.StringValue("project_uid"), - Items: []schedulesDatasourceItemModel{ - { - UpdateScheduleId: types.Int64Value(5), - Name: types.StringValue("update_schedule_name_1"), - Rrule: types.StringValue("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - Enabled: types.BoolValue(true), - MaintenanceWindow: types.Int64Value(1), - }, - }, - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - schedulesDataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &schedulesDataSourceModel{ - ProjectId: tt.expected.ProjectId, - ServerId: tt.expected.ServerId, - } - ctx := context.TODO() - err := mapSchedulesDatasourceFields(ctx, tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/serverupdate/serverupdate_acc_test.go b/stackit/internal/services/serverupdate/serverupdate_acc_test.go deleted file mode 100644 index 3d45a70a..00000000 --- a/stackit/internal/services/serverupdate/serverupdate_acc_test.go +++ /dev/null @@ -1,302 +0,0 @@ -package serverupdate_test - -import ( - "context" - _ "embed" - "fmt" - "log" - "maps" - "regexp" - "strconv" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - core_config "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/resource-min.tf - resourceMinConfig string - - //go:embed testdata/resource-max.tf - resourceMaxConfig string -) - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "server_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "schedule_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "rrule": config.StringVariable("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - "enabled": config.BoolVariable(true), - "maintenance_window": config.IntegerVariable(1), - "server_id": config.StringVariable(testutil.ServerId), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "server_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "schedule_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), - "rrule": config.StringVariable("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - "enabled": config.BoolVariable(true), - "maintenance_window": config.IntegerVariable(1), - "region": config.StringVariable("eu01"), - "server_id": config.StringVariable(testutil.ServerId), -} - -func configVarsInvalid(vars config.Variables) config.Variables { - tempConfig := maps.Clone(vars) - tempConfig["maintenance_window"] = config.IntegerVariable(0) - return tempConfig -} - -func configVarsMinUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMin) - tempConfig["maintenance_window"] = config.IntegerVariable(12) - tempConfig["rrule"] = config.StringVariable("DTSTART;TZID=Europe/Berlin:20250430T010000 RRULE:FREQ=DAILY;INTERVAL=3") - - return tempConfig -} - -func configVarsMaxUpdated() config.Variables { - tempConfig := maps.Clone(testConfigVarsMax) - tempConfig["maintenance_window"] = config.IntegerVariable(12) - tempConfig["rrule"] = config.StringVariable("DTSTART;TZID=Europe/Berlin:20250430T010000 RRULE:FREQ=DAILY;INTERVAL=3") - return tempConfig -} - -func TestAccServerUpdateScheduleMinResource(t *testing.T) { - if testutil.ServerId == "" { - fmt.Println("TF_ACC_SERVER_ID not set, skipping test") - return - } - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckServerUpdateScheduleDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: configVarsInvalid(configVarsMinUpdated()), - ExpectError: regexp.MustCompile(`.*maintenance_window value must be at least 1*`), - }, - // Creation - { - ConfigVariables: testConfigVarsMin, - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Update schedule data - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "server_id"), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "update_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "name", testutil.ConvertConfigVariable(testConfigVarsMin["schedule_name"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(testConfigVarsMin["rrule"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(testConfigVarsMin["enabled"])), - ), - }, - // data source - { - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Server update schedule data - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_update_schedule.test_schedule", "update_schedule_id"), - resource.TestCheckResourceAttrSet("data.stackit_server_update_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "name", testutil.ConvertConfigVariable(testConfigVarsMin["schedule_name"])), - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(testConfigVarsMin["rrule"])), - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(testConfigVarsMin["enabled"])), - - // Server update schedules data - resource.TestCheckResourceAttr("data.stackit_server_update_schedules.schedules_data_test", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_server_update_schedules.schedules_data_test", "server_id", testutil.ConvertConfigVariable(testConfigVarsMin["server_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_server_update_schedule.test_schedule", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server_update_schedule.test_schedule"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server_update_schedule.test_schedule") - } - scheduleId, ok := r.Primary.Attributes["update_schedule_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute update_schedule_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: configVarsMinUpdated(), - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMinConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Update schedule data - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "update_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "name", testutil.ConvertConfigVariable(configVarsMinUpdated()["schedule_name"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(configVarsMinUpdated()["rrule"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(configVarsMinUpdated()["enabled"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "maintenance_window", testutil.ConvertConfigVariable(configVarsMinUpdated()["maintenance_window"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccServerUpdateScheduleMaxResource(t *testing.T) { - if testutil.ServerId == "" { - fmt.Println("TF_ACC_SERVER_ID not set, skipping test") - return - } - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckServerUpdateScheduleDestroy, - Steps: []resource.TestStep{ - // Creation fail - { - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: configVarsInvalid(testConfigVarsMax), - ExpectError: regexp.MustCompile(`.*maintenance_window value must be at least 1*`), - }, - // Creation - { - ConfigVariables: testConfigVarsMax, - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Update schedule data - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "server_id"), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "update_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "name", testutil.ConvertConfigVariable(testConfigVarsMax["schedule_name"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(testConfigVarsMax["rrule"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(testConfigVarsMax["enabled"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "region", testutil.Region), - ), - }, - // data source - { - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Server update schedule data - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_update_schedule.test_schedule", "update_schedule_id"), - resource.TestCheckResourceAttrSet("data.stackit_server_update_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "name", testutil.ConvertConfigVariable(testConfigVarsMax["schedule_name"])), - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(testConfigVarsMax["rrule"])), - resource.TestCheckResourceAttr("data.stackit_server_update_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(testConfigVarsMax["enabled"])), - - // Server update schedules data - resource.TestCheckResourceAttr("data.stackit_server_update_schedules.schedules_data_test", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_server_update_schedules.schedules_data_test", "server_id", testutil.ConvertConfigVariable(testConfigVarsMax["server_id"])), - resource.TestCheckResourceAttrSet("data.stackit_server_update_schedules.schedules_data_test", "id"), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_server_update_schedule.test_schedule", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_server_update_schedule.test_schedule"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_server_update_schedule.test_schedule") - } - scheduleId, ok := r.Primary.Attributes["update_schedule_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute update_schedule_id") - } - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Update - { - ConfigVariables: configVarsMaxUpdated(), - Config: testutil.ServerUpdateProviderConfig() + "\n" + resourceMaxConfig, - Check: resource.ComposeAggregateTestCheckFunc( - // Update schedule data - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "update_schedule_id"), - resource.TestCheckResourceAttrSet("stackit_server_update_schedule.test_schedule", "id"), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "rrule", testutil.ConvertConfigVariable(configVarsMinUpdated()["rrule"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "enabled", testutil.ConvertConfigVariable(configVarsMinUpdated()["enabled"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "maintenance_window", testutil.ConvertConfigVariable(configVarsMinUpdated()["maintenance_window"])), - resource.TestCheckResourceAttr("stackit_server_update_schedule.test_schedule", "region", testutil.Region), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error { - ctx := context.Background() - if err := deleteSchedule(ctx, s); err != nil { - log.Printf("cannot delete schedule: %v", err) - } - - return nil -} - -func deleteSchedule(ctx context.Context, s *terraform.State) error { - var client *serverupdate.APIClient - var err error - if testutil.ServerUpdateCustomEndpoint == "" { - client, err = serverupdate.NewAPIClient() - } else { - client, err = serverupdate.NewAPIClient( - core_config.WithEndpoint(testutil.ServerUpdateCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - schedulesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_server_update_schedule" { - continue - } - // server update schedule terraform ID: "[project_id],[server_id],[update_schedule_id]" - scheduleId := strings.Split(rs.Primary.ID, core.Separator)[2] - schedulesToDestroy = append(schedulesToDestroy, scheduleId) - } - - schedulesResp, err := client.ListUpdateSchedules(ctx, testutil.ProjectId, testutil.ServerId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting schedulesResp: %w", err) - } - - schedules := *schedulesResp.Items - for i := range schedules { - if schedules[i].Id == nil { - continue - } - scheduleId := strconv.FormatInt(*schedules[i].Id, 10) - if utils.Contains(schedulesToDestroy, scheduleId) { - err := client.DeleteUpdateScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId, testutil.Region) - if err != nil { - return fmt.Errorf("destroying server update schedule %s during CheckDestroy: %w", scheduleId, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/serverupdate/testdata/resource-max.tf b/stackit/internal/services/serverupdate/testdata/resource-max.tf deleted file mode 100644 index 631c4fd8..00000000 --- a/stackit/internal/services/serverupdate/testdata/resource-max.tf +++ /dev/null @@ -1,29 +0,0 @@ -variable "project_id" {} -variable "server_name" {} -variable "schedule_name" {} -variable "rrule" {} -variable "enabled" {} -variable "maintenance_window" {} -variable "server_id" {} -variable "region" {} - -resource "stackit_server_update_schedule" "test_schedule" { - project_id = var.project_id - server_id = var.server_id - name = var.schedule_name - rrule = var.rrule - enabled = var.enabled - maintenance_window = var.maintenance_window - region = var.region -} - -data "stackit_server_update_schedule" "test_schedule" { - project_id = var.project_id - server_id = var.server_id - update_schedule_id = stackit_server_update_schedule.test_schedule.update_schedule_id -} - -data "stackit_server_update_schedules" "schedules_data_test" { - project_id = var.project_id - server_id = var.server_id -} diff --git a/stackit/internal/services/serverupdate/testdata/resource-min.tf b/stackit/internal/services/serverupdate/testdata/resource-min.tf deleted file mode 100644 index f2fd7a3b..00000000 --- a/stackit/internal/services/serverupdate/testdata/resource-min.tf +++ /dev/null @@ -1,27 +0,0 @@ -variable "project_id" {} -variable "server_name" {} -variable "schedule_name" {} -variable "rrule" {} -variable "enabled" {} -variable "maintenance_window" {} -variable "server_id" {} - -resource "stackit_server_update_schedule" "test_schedule" { - project_id = var.project_id - server_id = var.server_id - name = var.schedule_name - rrule = var.rrule - enabled = var.enabled - maintenance_window = var.maintenance_window -} - -data "stackit_server_update_schedule" "test_schedule" { - project_id = var.project_id - server_id = var.server_id - update_schedule_id = stackit_server_update_schedule.test_schedule.update_schedule_id -} - -data "stackit_server_update_schedules" "schedules_data_test" { - project_id = var.project_id - server_id = var.server_id -} diff --git a/stackit/internal/services/serverupdate/utils/util.go b/stackit/internal/services/serverupdate/utils/util.go deleted file mode 100644 index cfd39299..00000000 --- a/stackit/internal/services/serverupdate/utils/util.go +++ /dev/null @@ -1,30 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *serverupdate.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ServerUpdateCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ServerUpdateCustomEndpoint)) - } - apiClient, err := serverupdate.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/serverupdate/utils/util_test.go b/stackit/internal/services/serverupdate/utils/util_test.go deleted file mode 100644 index 1b687588..00000000 --- a/stackit/internal/services/serverupdate/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://serverupdate-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *serverupdate.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *serverupdate.APIClient { - apiClient, err := serverupdate.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ServerUpdateCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *serverupdate.APIClient { - apiClient, err := serverupdate.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/serviceaccount/account/datasource.go b/stackit/internal/services/serviceaccount/account/datasource.go deleted file mode 100644 index be6e0cca..00000000 --- a/stackit/internal/services/serviceaccount/account/datasource.go +++ /dev/null @@ -1,157 +0,0 @@ -package account - -import ( - "context" - "fmt" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &serviceAccountDataSource{} -) - -// NewServiceAccountDataSource creates a new instance of the serviceAccountDataSource. -func NewServiceAccountDataSource() datasource.DataSource { - return &serviceAccountDataSource{} -} - -// serviceAccountDataSource is the datasource implementation for service accounts. -type serviceAccountDataSource struct { - client *serviceaccount.APIClient -} - -// Configure initializes the serviceAccountDataSource with the provided provider data. -func (r *serviceAccountDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Service Account client configured") -} - -// Metadata provides metadata for the service account datasource. -func (r *serviceAccountDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_service_account" -} - -// Schema defines the schema for the service account data source. -func (r *serviceAccountDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "id": "Terraform's internal resource ID, structured as \"`project_id`,`email`\".", - "project_id": "STACKIT project ID to which the service account is associated.", - "name": "Name of the service account.", - "email": "Email of the service account.", - } - - // Define the schema with validation rules and descriptions for each attribute. - // The datasource schema differs slightly from the resource schema. - // In this case, the email attribute is required to read the service account data from the API. - resp.Schema = schema.Schema{ - MarkdownDescription: "Service account data source schema.", - Description: "Service account data source schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "email": schema.StringAttribute{ - Description: descriptions["email"], - Required: true, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - }, - } -} - -// Read reads all service accounts from the API and updates the state with the latest information. -func (r *serviceAccountDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID from the model configuration - projectId := model.ProjectId.ValueString() - - // Call the API to list service accounts in the specified project - listSaResp, err := r.client.ListServiceAccounts(ctx, projectId).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading service account", - fmt.Sprintf("Forbidden access for service account in project %q.", projectId), - map[int]string{}, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Iterate over the service accounts returned by the API to find the one matching the email - serviceAccounts := *listSaResp.Items - for i := range serviceAccounts { - // Skip if the service account email does not match - if *serviceAccounts[i].Email != model.Email.ValueString() { - continue - } - - // Map the API response to the model, updating its fields with the service account data - err = mapFields(&serviceAccounts[i], &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account", fmt.Sprintf("Error processing API response: %v", err)) - return - } - - // Try to parse the name from the provided email address - name, err := parseNameFromEmail(model.Email.ValueString()) - if name != "" && err == nil { - model.Name = types.StringValue(name) - } - - // Update the state with the service account model - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - return - } - - // If no matching service account is found, remove the resource from the state - core.LogAndAddError(ctx, &resp.Diagnostics, "Reading service account", "Service account not found") - resp.State.RemoveResource(ctx) -} diff --git a/stackit/internal/services/serviceaccount/account/resource.go b/stackit/internal/services/serviceaccount/account/resource.go deleted file mode 100644 index 1be909e0..00000000 --- a/stackit/internal/services/serviceaccount/account/resource.go +++ /dev/null @@ -1,340 +0,0 @@ -package account - -import ( - "context" - "fmt" - "regexp" - "strings" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &serviceAccountResource{} - _ resource.ResourceWithConfigure = &serviceAccountResource{} - _ resource.ResourceWithImportState = &serviceAccountResource{} -) - -// Model represents the schema for the service account resource. -type Model struct { - Id types.String `tfsdk:"id"` // Required by Terraform - ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the service account - Name types.String `tfsdk:"name"` // Name of the service account - Email types.String `tfsdk:"email"` // Email linked to the service account -} - -// NewServiceAccountResource is a helper function to create a new service account resource instance. -func NewServiceAccountResource() resource.Resource { - return &serviceAccountResource{} -} - -// serviceAccountResource implements the resource interface for service accounts. -type serviceAccountResource struct { - client *serviceaccount.APIClient -} - -// Configure sets up the API client for the service account resource. -func (r *serviceAccountResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Service Account client configured") -} - -// Metadata sets the resource type name for the service account resource. -func (r *serviceAccountResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_service_account" -} - -// Schema defines the schema for the resource. -func (r *serviceAccountResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "id": "Terraform's internal resource ID, structured as \"`project_id`,`email`\".", - "project_id": "STACKIT project ID to which the service account is associated.", - "name": "Name of the service account.", - "email": "Email of the service account.", - } - - resp.Schema = schema.Schema{ - MarkdownDescription: "Service account resource schema.", - Description: "Service account resource schema.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.LengthAtMost(20), - stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z](?:-?[a-z0-9]+)*$`), "must start with a lowercase letter, can contain lowercase letters, numbers, and dashes, but cannot start or end with a dash, and dashes cannot be consecutive"), - }, - }, - "email": schema.StringAttribute{ - Description: descriptions["email"], - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state for service accounts. -func (r *serviceAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the planned values for the resource. - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Set logging context with the project ID and service account name. - projectId := model.ProjectId.ValueString() - serviceAccountName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "service_account_name", serviceAccountName) - - // Generate the API request payload. - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Create the new service account via the API client. - serviceAccountResp, err := r.client.CreateServiceAccount(ctx, projectId).CreateServiceAccountPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Set the service account name and map the response to the resource schema. - model.Name = types.StringValue(serviceAccountName) - err = mapFields(serviceAccountResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account", fmt.Sprintf("Processing API response: %v", err)) - return - } - - // This sleep is currently needed due to the IAM Cache. - time.Sleep(5 * time.Second) - - // Set the state with fully populated data. - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Service account created") -} - -// Read refreshes the Terraform state with the latest service account data. -func (r *serviceAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID for the service account. - projectId := model.ProjectId.ValueString() - - // Fetch the list of service accounts from the API. - listSaResp, err := r.client.ListServiceAccounts(ctx, projectId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account", fmt.Sprintf("Error calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Iterate over the list of service accounts to find the one that matches the email from the state. - serviceAccounts := *listSaResp.Items - for i := range serviceAccounts { - if *serviceAccounts[i].Email != model.Email.ValueString() { - continue - } - - // Map the response data to the resource schema and update the state. - err = mapFields(&serviceAccounts[i], &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account", fmt.Sprintf("Error processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - return - } - - // If no matching service account is found, remove the resource from the state. - resp.State.RemoveResource(ctx) -} - -// Update attempts to update the resource. In this case, service accounts cannot be updated. -// Note: This method is intentionally left without update logic because changes -// to 'project_id' or 'name' require the resource to be entirely replaced. -// As a result, the Update function is redundant since any modifications will -// automatically trigger a resource recreation through Terraform's built-in -// lifecycle management. -func (r *serviceAccountResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Service accounts cannot be updated, so we log an error. - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account", "Service accounts can't be updated") -} - -// Delete deletes the service account and removes it from the Terraform state on success. -func (r *serviceAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serviceAccountName := model.Name.ValueString() - serviceAccountEmail := model.Email.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "service_account_name", serviceAccountName) - - // Call API to delete the existing service account. - err := r.client.DeleteServiceAccount(ctx, projectId, serviceAccountEmail).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting service account", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Service account deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,email -func (r *serviceAccountResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - // Split the import identifier to extract project ID and email. - idParts := strings.Split(req.ID, core.Separator) - - // Ensure the import identifier format is correct. - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing service account", - fmt.Sprintf("Expected import identifier with format: [project_id],[email] Got: %q", req.ID), - ) - return - } - - projectId := idParts[0] - email := idParts[1] - - // Attempt to parse the name from the email if valid. - name, err := parseNameFromEmail(email) - if name != "" && err == nil { - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...) - } - - // Set the project ID and email attributes in the state. - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("email"), email)...) - tflog.Info(ctx, "Service account state imported") -} - -// toCreatePayload generates the payload to create a new service account. -func toCreatePayload(model *Model) (*serviceaccount.CreateServiceAccountPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &serviceaccount.CreateServiceAccountPayload{ - Name: conversion.StringValueToPointer(model.Name), - }, nil -} - -// mapFields maps a ServiceAccount response to the model. -func mapFields(resp *serviceaccount.ServiceAccount, model *Model) error { - if resp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if resp.Email == nil { - return fmt.Errorf("service account email not present") - } - - // Build the ID by combining the project ID and email and assign the model's fields. - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), *resp.Email) - model.Email = types.StringPointerValue(resp.Email) - model.ProjectId = types.StringPointerValue(resp.ProjectId) - - return nil -} - -// parseNameFromEmail extracts the name component from an email address. -// The email format must be `name-@sa.stackit.cloud`. -func parseNameFromEmail(email string) (string, error) { - namePattern := `^([a-z][a-z0-9]*(?:-[a-z0-9]+)*)-\w{7}@sa\.stackit\.cloud$` - re := regexp.MustCompile(namePattern) - match := re.FindStringSubmatch(email) - - // If a match is found, return the name component - if len(match) > 1 { - return match[1], nil - } - - // If no match is found, return an error - return "", fmt.Errorf("unable to parse name from email") -} diff --git a/stackit/internal/services/serviceaccount/account/resource_test.go b/stackit/internal/services/serviceaccount/account/resource_test.go deleted file mode 100644 index 14cbb992..00000000 --- a/stackit/internal/services/serviceaccount/account/resource_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package account - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" -) - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *serviceaccount.CreateServiceAccountPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &serviceaccount.CreateServiceAccountPayload{ - Name: nil, - }, - true, - }, - { - "default_values", - &Model{ - Name: types.StringValue("example-name1"), - }, - &serviceaccount.CreateServiceAccountPayload{ - Name: utils.Ptr("example-name1"), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapFields(t *testing.T) { - tests := []struct { - description string - input *serviceaccount.ServiceAccount - expected Model - isValid bool - }{ - { - "default_values", - &serviceaccount.ServiceAccount{ - ProjectId: utils.Ptr("pid"), - Email: utils.Ptr("mail"), - }, - Model{ - Id: types.StringValue("pid,mail"), - ProjectId: types.StringValue("pid"), - Email: types.StringValue("mail"), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "nil_response_2", - &serviceaccount.ServiceAccount{}, - Model{}, - false, - }, - { - "no_id", - &serviceaccount.ServiceAccount{ - ProjectId: utils.Ptr("pid"), - Internal: utils.Ptr(true), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - } - err := mapFields(tt.input, state) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestParseNameFromEmail(t *testing.T) { - testCases := []struct { - email string - expected string - shouldError bool - }{ - {"test03-8565oq1@sa.stackit.cloud", "test03", false}, - {"import-test-vshp191@sa.stackit.cloud", "import-test", false}, - {"sa-test-01-acfj2s1@sa.stackit.cloud", "sa-test-01", false}, - {"invalid-email@sa.stackit.cloud", "", true}, - {"missingcode-@sa.stackit.cloud", "", true}, - {"nohyphen8565oq1@sa.stackit.cloud", "", true}, - {"eu01-qnmbwo1@unknown.stackit.cloud", "", true}, - {"eu01-qnmbwo1@ske.stackit.com", "", true}, - {"someotherformat@sa.stackit.cloud", "", true}, - } - - for _, tc := range testCases { - t.Run(tc.email, func(t *testing.T) { - name, err := parseNameFromEmail(tc.email) - if tc.shouldError { - if err == nil { - t.Errorf("expected an error for email: %s, but got none", tc.email) - } - } else { - if err != nil { - t.Errorf("did not expect an error for email: %s, but got: %v", tc.email, err) - } - if name != tc.expected { - t.Errorf("expected name: %s, got: %s for email: %s", tc.expected, name, tc.email) - } - } - }) - } -} diff --git a/stackit/internal/services/serviceaccount/key/const.go b/stackit/internal/services/serviceaccount/key/const.go deleted file mode 100644 index 5ebeea5f..00000000 --- a/stackit/internal/services/serviceaccount/key/const.go +++ /dev/null @@ -1,26 +0,0 @@ -package key - -const markdownDescription = ` -## Example Usage` + "\n" + ` - -### Automatically rotate service account keys` + "\n" + - "```terraform" + ` -resource "stackit_service_account" "sa" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "sa01" -} - -resource "time_rotating" "rotate" { - rotation_days = 80 -} - -resource "stackit_service_account_key" "sa_key" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - service_account_email = stackit_service_account.sa.email - ttl_days = 90 - - rotate_when_changed = { - rotation = time_rotating.rotate.id - } -} -` + "\n```" diff --git a/stackit/internal/services/serviceaccount/key/resource.go b/stackit/internal/services/serviceaccount/key/resource.go deleted file mode 100644 index ea254c94..00000000 --- a/stackit/internal/services/serviceaccount/key/resource.go +++ /dev/null @@ -1,352 +0,0 @@ -package key - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "time" - - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &serviceAccountKeyResource{} - _ resource.ResourceWithConfigure = &serviceAccountKeyResource{} -) - -// Model represents the schema for the service account key resource in Terraform. -type Model struct { - Id types.String `tfsdk:"id"` - KeyId types.String `tfsdk:"key_id"` - ServiceAccountEmail types.String `tfsdk:"service_account_email"` - ProjectId types.String `tfsdk:"project_id"` - RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"` - TtlDays types.Int64 `tfsdk:"ttl_days"` - PublicKey types.String `tfsdk:"public_key"` - Json types.String `tfsdk:"json"` -} - -// NewServiceAccountKeyResource is a helper function to create a new service account key resource instance. -func NewServiceAccountKeyResource() resource.Resource { - return &serviceAccountKeyResource{} -} - -// serviceAccountKeyResource implements the resource interface for service account key. -type serviceAccountKeyResource struct { - client *serviceaccount.APIClient -} - -// Configure sets up the API client for the service account resource. -func (r *serviceAccountKeyResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Service Account client configured") -} - -// Metadata sets the resource type name for the service account key resource. -func (r *serviceAccountKeyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_service_account_key" -} - -// Schema defines the resource schema for the service account access key. -func (r *serviceAccountKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`service_account_email`,`key_id`\".", - "main": "Service account key schema.", - "project_id": "The STACKIT project ID associated with the service account key.", - "key_id": "The unique identifier for the key associated with the service account.", - "service_account_email": "The email address associated with the service account, used for account identification and communication.", - "ttl_days": "Specifies the key's validity duration in days. If left unspecified, the key is considered valid until it is deleted", - "rotate_when_changed": "A map of arbitrary key/value pairs designed to force key recreation when they change, facilitating key rotation based on external factors such as a changing timestamp. Modifying this map triggers the creation of a new resource.", - "public_key": "Specifies the public_key (RSA2048 key-pair). If not provided, a certificate from STACKIT will be used to generate a private_key.", - "json": "The raw JSON representation of the service account key json, available for direct use.", - } - resp.Schema = schema.Schema{ - MarkdownDescription: fmt.Sprintf("%s%s", descriptions["main"], markdownDescription), - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "service_account_email": schema.StringAttribute{ - Description: descriptions["service_account_email"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "public_key": schema.StringAttribute{ - Description: descriptions["public_key"], - Optional: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "ttl_days": schema.Int64Attribute{ - Description: descriptions["ttl_days"], - Optional: true, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - }, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - }, - "rotate_when_changed": schema.MapAttribute{ - Description: descriptions["rotate_when_changed"], - Optional: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.Map{ - mapplanmodifier.RequiresReplace(), - }, - }, - "key_id": schema.StringAttribute{ - Description: descriptions["key_id"], - Computed: true, - }, - "json": schema.StringAttribute{ - Description: descriptions["json"], - Computed: true, - Sensitive: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state for service accounts. -func (r *serviceAccountKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the planned values for the resource. - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Set logging context with the project ID and service account email. - projectId := model.ProjectId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - - if utils.IsUndefined(model.TtlDays) { - model.TtlDays = types.Int64Null() - } - - // Generate the API request payload. - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account key", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Initialize the API request with the required parameters. - saAccountKeyResp, err := r.client.CreateServiceAccountKey(ctx, projectId, serviceAccountEmail).CreateServiceAccountKeyPayload(*payload).Execute() - - ctx = core.LogResponse(ctx) - - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Failed to create service account key", fmt.Sprintf("API call error: %v", err)) - return - } - - // Map the response to the resource schema. - err = mapCreateResponse(saAccountKeyResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account key", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set the state with fully populated data. - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Service account key created") -} - -// Read refreshes the Terraform state with the latest service account data. -func (r *serviceAccountKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve the current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - keyId := model.KeyId.ValueString() - - _, err := r.client.GetServiceAccountKey(ctx, projectId, serviceAccountEmail, keyId).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) - // due to security purposes, attempting to get access key for a non-existent Service Account will return 403. - if ok && oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusForbidden || oapiErr.StatusCode == http.StatusBadRequest { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account key", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // No mapping needed for read response, as private_key is excluded and ID remains unchanged. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "key read") -} - -// Update attempts to update the resource. In this case, service account key cannot be updated. -// Note: This method is intentionally left without update logic because changes -// to 'project_id', 'service_account_email', 'ttl_days' or 'public_key' require the resource to be entirely replaced. -// As a result, the Update function is redundant since any modifications will -// automatically trigger a resource recreation through Terraform's built-in -// lifecycle management. -func (r *serviceAccountKeyResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Service accounts cannot be updated, so we log an error. - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account key", "Service account key can't be updated") -} - -// Delete deletes the service account key and removes it from the Terraform state on success. -func (r *serviceAccountKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - keyId := model.KeyId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - ctx = tflog.SetField(ctx, "key_id", keyId) - - // Call API to delete the existing service account key. - err := r.client.DeleteServiceAccountKey(ctx, projectId, serviceAccountEmail, keyId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting service account key", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Service account key deleted") -} - -func toCreatePayload(model *Model) (*serviceaccount.CreateServiceAccountKeyPayload, error) { - if model == nil { - return nil, fmt.Errorf("model is nil") - } - - // Prepare the payload - payload := &serviceaccount.CreateServiceAccountKeyPayload{} - - // Set ValidUntil based on TtlDays if specified - if !utils.IsUndefined(model.TtlDays) { - validUntil, err := computeValidUntil(model.TtlDays.ValueInt64Pointer()) - if err != nil { - return nil, err - } - payload.ValidUntil = &validUntil - } - - // Set PublicKey if specified - if !utils.IsUndefined(model.PublicKey) && model.PublicKey.ValueString() != "" { - payload.PublicKey = conversion.StringValueToPointer(model.PublicKey) - } - - return payload, nil -} - -// computeValidUntil calculates the timestamp for when the item will no longer be valid. -func computeValidUntil(ttlDays *int64) (time.Time, error) { - if ttlDays == nil { - return time.Time{}, fmt.Errorf("ttlDays is nil") - } - return time.Now().UTC().Add(time.Duration(*ttlDays) * 24 * time.Hour), nil -} - -// mapCreateResponse maps response data from a create operation to the model. -func mapCreateResponse(resp *serviceaccount.CreateServiceAccountKeyResponse, model *Model) error { - if model == nil { - return fmt.Errorf("model input is nil") - } - - if resp == nil { - return fmt.Errorf("service account key response is nil") - } - - if resp.Id == nil { - return fmt.Errorf("service account key id not present") - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.ServiceAccountEmail.ValueString(), *resp.Id) - model.KeyId = types.StringPointerValue(resp.Id) - - jsonData, err := json.Marshal(resp) - if err != nil { - return fmt.Errorf("JSON encoding error: %w", err) - } - - if jsonData != nil { - model.Json = types.StringValue(string(jsonData)) - } - - return nil -} diff --git a/stackit/internal/services/serviceaccount/key/resource_test.go b/stackit/internal/services/serviceaccount/key/resource_test.go deleted file mode 100644 index 15f90a1b..00000000 --- a/stackit/internal/services/serviceaccount/key/resource_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package key - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" -) - -func TestComputeValidUntil(t *testing.T) { - tests := []struct { - name string - ttlDays *int - isValid bool - expected time.Time - }{ - { - name: "ttlDays is 10", - ttlDays: utils.Ptr(10), - isValid: true, - expected: time.Now().UTC().Add(time.Duration(10) * 24 * time.Hour), - }, - { - name: "ttlDays is 0", - ttlDays: utils.Ptr(0), - isValid: true, - expected: time.Now().UTC().Add(time.Duration(0) * 24 * time.Hour), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - int64TTlDays := int64(*tt.ttlDays) - validUntil, err := computeValidUntil(&int64TTlDays) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - tolerance := 1 * time.Second - if validUntil.Sub(tt.expected) > tolerance && tt.expected.Sub(validUntil) > tolerance { - t.Fatalf("Times do not match. got: %v expected: %v", validUntil, tt.expected) - } - } - }) - } -} - -func TestMapResponse(t *testing.T) { - tests := []struct { - description string - input *serviceaccount.CreateServiceAccountKeyResponse - expected Model - isValid bool - }{ - { - description: "default_values", - input: &serviceaccount.CreateServiceAccountKeyResponse{ - Id: utils.Ptr("id"), - }, - expected: Model{ - Id: types.StringValue("pid,email,id"), - KeyId: types.StringValue("id"), - ProjectId: types.StringValue("pid"), - ServiceAccountEmail: types.StringValue("email"), - Json: types.StringValue("{}"), - RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - isValid: true, - }, - { - description: "nil_response", - input: nil, - expected: Model{}, - isValid: false, - }, - { - description: "nil_response_2", - input: &serviceaccount.CreateServiceAccountKeyResponse{}, - expected: Model{}, - isValid: false, - }, - { - description: "no_id", - input: &serviceaccount.CreateServiceAccountKeyResponse{ - Active: utils.Ptr(true), - }, - expected: Model{}, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - ServiceAccountEmail: tt.expected.ServiceAccountEmail, - KeyId: types.StringNull(), - Json: types.StringValue("{}"), - RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), - } - err := mapCreateResponse(tt.input, model) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - model.Json = types.StringValue("{}") - diff := cmp.Diff(*model, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go b/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go deleted file mode 100644 index 032dae78..00000000 --- a/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package serviceaccount - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -// Service Account resource data -var serviceAccountResource = map[string]string{ - "project_id": testutil.ProjectId, - "name01": "sa-test-01", - "name02": "sa-test-02", -} - -func inputServiceAccountResourceConfig(name string) string { - return fmt.Sprintf(` - %s - - resource "stackit_service_account" "sa" { - project_id = "%s" - name = "%s" - } - - resource "stackit_service_account_access_token" "token" { - project_id = stackit_service_account.sa.project_id - service_account_email = stackit_service_account.sa.email - } - - resource "stackit_service_account_key" "key" { - project_id = stackit_service_account.sa.project_id - service_account_email = stackit_service_account.sa.email - ttl_days = 90 - } - `, - testutil.ServiceAccountProviderConfig(), - serviceAccountResource["project_id"], - name, - ) -} - -func inputServiceAccountDataSourceConfig() string { - return fmt.Sprintf(` - %s - - data "stackit_service_account" "sa" { - project_id = stackit_service_account.sa.project_id - email = stackit_service_account.sa.email - } - `, - inputServiceAccountResourceConfig(serviceAccountResource["name01"]), - ) -} - -func TestServiceAccount(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckServiceAccountDestroy, - Steps: []resource.TestStep{ - // Creation - { - Config: inputServiceAccountResourceConfig(serviceAccountResource["name01"]), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_service_account.sa", "project_id", serviceAccountResource["project_id"]), - resource.TestCheckResourceAttr("stackit_service_account.sa", "name", serviceAccountResource["name01"]), - resource.TestCheckResourceAttrSet("stackit_service_account.sa", "email"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "token"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "created_at"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "valid_until"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "service_account_email"), - resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "ttl_days"), - resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "json"), - resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "service_account_email"), - resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_access_token.token", "service_account_email"), - resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_key.key", "service_account_email"), - ), - }, - // Update - { - Config: inputServiceAccountResourceConfig(serviceAccountResource["name02"]), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr("stackit_service_account.sa", "project_id", serviceAccountResource["project_id"]), - resource.TestCheckResourceAttr("stackit_service_account.sa", "name", serviceAccountResource["name02"]), - resource.TestCheckResourceAttrSet("stackit_service_account.sa", "email"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "token"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "created_at"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "valid_until"), - resource.TestCheckResourceAttrSet("stackit_service_account_access_token.token", "service_account_email"), - resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "ttl_days"), - resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "json"), - resource.TestCheckResourceAttrSet("stackit_service_account_key.key", "service_account_email"), - resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_access_token.token", "service_account_email"), - resource.TestCheckResourceAttrPair("stackit_service_account.sa", "email", "stackit_service_account_key.key", "service_account_email"), - ), - }, - // Data source - { - Config: inputServiceAccountDataSourceConfig(), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("data.stackit_service_account.sa", "project_id", serviceAccountResource["project_id"]), - resource.TestCheckResourceAttrPair( - "stackit_service_account.sa", "project_id", - "data.stackit_service_account.sa", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_service_account.sa", "name", - "data.stackit_service_account.sa", "name", - ), - resource.TestCheckResourceAttrPair( - "stackit_service_account.sa", "email", - "data.stackit_service_account.sa", "email", - ), - ), - }, - // Import - { - ResourceName: "stackit_service_account.sa", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_service_account.sa"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_service_account.sa") - } - email, ok := r.Primary.Attributes["email"] - if !ok { - return "", fmt.Errorf("couldn't find attribute email") - } - return fmt.Sprintf("%s,%s", testutil.ProjectId, email), nil - }, - ImportState: true, - ImportStateVerify: true, - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckServiceAccountDestroy(s *terraform.State) error { - ctx := context.Background() - var client *serviceaccount.APIClient - var err error - - if testutil.ServiceAccountCustomEndpoint == "" { - client, err = serviceaccount.NewAPIClient() - } else { - client, err = serviceaccount.NewAPIClient( - config.WithEndpoint(testutil.ServiceAccountCustomEndpoint), - ) - } - - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - var instancesToDestroy []string - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_service_account" { - continue - } - serviceAccountEmail := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, serviceAccountEmail) - } - - instancesResp, err := client.ListServiceAccounts(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting service accounts: %w", err) - } - - serviceAccounts := *instancesResp.Items - for i := range serviceAccounts { - if serviceAccounts[i].Email == nil { - continue - } - if utils.Contains(instancesToDestroy, *serviceAccounts[i].Email) { - err := client.DeleteServiceAccount(ctx, testutil.ProjectId, *serviceAccounts[i].Email).Execute() - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *serviceAccounts[i].Email, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/serviceaccount/token/const.go b/stackit/internal/services/serviceaccount/token/const.go deleted file mode 100644 index 94f9377c..00000000 --- a/stackit/internal/services/serviceaccount/token/const.go +++ /dev/null @@ -1,26 +0,0 @@ -package token - -const markdownDescription = ` -## Example Usage` + "\n" + ` - -### Automatically rotate access tokens` + "\n" + - "```terraform" + ` -resource "stackit_service_account" "sa" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - name = "sa01" -} - -resource "time_rotating" "rotate" { - rotation_days = 80 -} - -resource "stackit_service_account_access_token" "sa_token" { - project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" - service_account_email = stackit_service_account.sa.email - ttl_days = 180 - - rotate_when_changed = { - rotation = time_rotating.rotate.id - } -} -` + "\n```" diff --git a/stackit/internal/services/serviceaccount/token/resource.go b/stackit/internal/services/serviceaccount/token/resource.go deleted file mode 100644 index b4923539..00000000 --- a/stackit/internal/services/serviceaccount/token/resource.go +++ /dev/null @@ -1,405 +0,0 @@ -package token - -import ( - "context" - "errors" - "fmt" - "net/http" - "time" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "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/mapplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &serviceAccountTokenResource{} - _ resource.ResourceWithConfigure = &serviceAccountTokenResource{} -) - -// Model represents the schema for the service account token resource in Terraform. -type Model struct { - Id types.String `tfsdk:"id"` - AccessTokenId types.String `tfsdk:"access_token_id"` - ServiceAccountEmail types.String `tfsdk:"service_account_email"` - ProjectId types.String `tfsdk:"project_id"` - TtlDays types.Int64 `tfsdk:"ttl_days"` - RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"` - Token types.String `tfsdk:"token"` - Active types.Bool `tfsdk:"active"` - CreatedAt types.String `tfsdk:"created_at"` - ValidUntil types.String `tfsdk:"valid_until"` -} - -// NewServiceAccountTokenResource is a helper function to create a new service account access token resource instance. -func NewServiceAccountTokenResource() resource.Resource { - return &serviceAccountTokenResource{} -} - -// serviceAccountResource implements the resource interface for service account access token. -type serviceAccountTokenResource struct { - client *serviceaccount.APIClient -} - -// Configure sets up the API client for the service account resource. -func (r *serviceAccountTokenResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "Service Account client configured") -} - -// Metadata sets the resource type name for the service account resource. -func (r *serviceAccountTokenResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_service_account_access_token" -} - -// Schema defines the resource schema for the service account access token. -func (r *serviceAccountTokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`service_account_email`,`access_token_id`\".", - "main": "Service account access token schema.", - "deprecation_message": "This resource is scheduled for deprecation and will be removed on December 17, 2025. To ensure a smooth transition, please refer to our migration guide at https://docs.stackit.cloud/platform/access-and-identity/service-accounts/migrate-flows/ for detailed instructions and recommendations.", - "project_id": "STACKIT project ID associated with the service account token.", - "service_account_email": "Email address linked to the service account.", - "ttl_days": "Specifies the token's validity duration in days. If unspecified, defaults to 90 days.", - "rotate_when_changed": "A map of arbitrary key/value pairs that will force recreation of the token when they change, enabling token rotation based on external conditions such as a rotating timestamp. Changing this forces a new resource to be created.", - "access_token_id": "Identifier for the access token linked to the service account.", - "token": "JWT access token for API authentication. Prefixed by 'Bearer' and should be stored securely as it is irretrievable once lost.", - "active": "Indicate whether the token is currently active or inactive", - "created_at": "Timestamp indicating when the access token was created.", - "valid_until": "Estimated expiration timestamp of the access token. For precise validity, check the JWT details.", - } - resp.Schema = schema.Schema{ - MarkdownDescription: fmt.Sprintf("%s\n\n!> %s\n%s", descriptions["main"], descriptions["deprecation_message"], markdownDescription), - Description: descriptions["main"], - DeprecationMessage: descriptions["deprecation_message"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "service_account_email": schema.StringAttribute{ - Description: descriptions["service_account_email"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "ttl_days": schema.Int64Attribute{ - Description: descriptions["ttl_days"], - Optional: true, - Computed: true, - Validators: []validator.Int64{ - int64validator.Between(1, 180), - }, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Default: int64default.StaticInt64(90), - }, - "rotate_when_changed": schema.MapAttribute{ - Description: descriptions["rotate_when_changed"], - Optional: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.Map{ - mapplanmodifier.RequiresReplace(), - }, - }, - "access_token_id": schema.StringAttribute{ - Description: descriptions["access_token_id"], - Computed: true, - }, - "token": schema.StringAttribute{ - Description: descriptions["token"], - Computed: true, - Sensitive: true, - }, - "active": schema.BoolAttribute{ - Description: descriptions["active"], - Computed: true, - }, - "created_at": schema.StringAttribute{ - Description: descriptions["created_at"], - Computed: true, - }, - "valid_until": schema.StringAttribute{ - Description: descriptions["valid_until"], - Computed: true, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state for service accounts. -func (r *serviceAccountTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - core.LogAndAddWarning(ctx, &resp.Diagnostics, "stackit_service_account_access_token resource deprecated", "use stackit_service_account_key resource instead") - // Retrieve the planned values for the resource. - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Set logging context with the project ID and service account email. - projectId := model.ProjectId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - - // Generate the API request payload. - payload, err := toCreatePayload(&model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account access token", fmt.Sprintf("Creating API payload: %v", err)) - return - } - - // Initialize the API request with the required parameters. - serviceAccountAccessTokenResp, err := r.client.CreateAccessToken(ctx, projectId, serviceAccountEmail).CreateAccessTokenPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Failed to create service account access token", fmt.Sprintf("API call error: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map the response to the resource schema. - err = mapCreateResponse(serviceAccountAccessTokenResp, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account access token", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set the state with fully populated data. - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "Service account access token created") -} - -// Read refreshes the Terraform state with the latest service account data. -func (r *serviceAccountTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - core.LogAndAddWarning(ctx, &resp.Diagnostics, "stackit_service_account_access_token resource deprecated", "use stackit_service_account_key resource instead") - // Retrieve the current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - // Extract the project ID and serviceAccountEmail for the service account. - projectId := model.ProjectId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - - // Fetch the list of service account tokens from the API. - listSaTokensResp, err := r.client.ListAccessTokens(ctx, projectId, serviceAccountEmail).Execute() - if err != nil { - var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - // due to security purposes, attempting to list access tokens for a non-existent Service Account will return 403. - if ok && oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusForbidden { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account tokens", fmt.Sprintf("Error calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Iterate over the list of service account tokens to find the one that matches the ID from the state. - saTokens := *listSaTokensResp.Items - for i := range saTokens { - if *saTokens[i].Id != model.AccessTokenId.ValueString() { - continue - } - - if !*saTokens[i].Active { - tflog.Info(ctx, fmt.Sprintf("Service account access token with id %s is not active", model.AccessTokenId.ValueString())) - resp.State.RemoveResource(ctx) - return - } - - err = mapListResponse(&saTokens[i], &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account", fmt.Sprintf("Error processing API response: %v", err)) - return - } - - // Set the updated state. - diags = resp.State.Set(ctx, &model) - resp.Diagnostics.Append(diags...) - return - } - // If no matching service account access token is found, remove the resource from the state. - tflog.Info(ctx, fmt.Sprintf("Service account access token with id %s not found", model.AccessTokenId.ValueString())) - resp.State.RemoveResource(ctx) -} - -// Update attempts to update the resource. In this case, service account token cannot be updated. -// Note: This method is intentionally left without update logic because changes -// to 'project_id', 'service_account_email' or 'ttl_days' require the resource to be entirely replaced. -// As a result, the Update function is redundant since any modifications will -// automatically trigger a resource recreation through Terraform's built-in -// lifecycle management. -func (r *serviceAccountTokenResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Service accounts cannot be updated, so we log an error. - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account access token", "Service accounts can't be updated") -} - -// Delete deletes the service account and removes it from the Terraform state on success. -func (r *serviceAccountTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - core.LogAndAddWarning(ctx, &resp.Diagnostics, "stackit_service_account_access_token resource deprecated", "use stackit_service_account_key resource instead") - // Retrieve current state of the resource. - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - serviceAccountEmail := model.ServiceAccountEmail.ValueString() - accessTokenId := model.AccessTokenId.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail) - ctx = tflog.SetField(ctx, "access_token_id", accessTokenId) - - // Call API to delete the existing service account. - err := r.client.DeleteAccessToken(ctx, projectId, serviceAccountEmail, accessTokenId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting service account token", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "Service account token deleted") -} - -func toCreatePayload(model *Model) (*serviceaccount.CreateAccessTokenPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &serviceaccount.CreateAccessTokenPayload{ - TtlDays: conversion.Int64ValueToPointer(model.TtlDays), - }, nil -} - -func mapCreateResponse(resp *serviceaccount.AccessToken, model *Model) error { - if resp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if resp.Token == nil { - return fmt.Errorf("service account token not present") - } - - if resp.Id == nil { - return fmt.Errorf("service account id not present") - } - - var createdAt basetypes.StringValue - if resp.CreatedAt != nil { - createdAtValue := *resp.CreatedAt - createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - - var validUntil basetypes.StringValue - if resp.ValidUntil != nil { - validUntilValue := *resp.ValidUntil - validUntil = types.StringValue(validUntilValue.Format(time.RFC3339)) - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.ServiceAccountEmail.ValueString(), *resp.Id) - model.AccessTokenId = types.StringPointerValue(resp.Id) - model.Token = types.StringPointerValue(resp.Token) - model.Active = types.BoolPointerValue(resp.Active) - model.CreatedAt = createdAt - model.ValidUntil = validUntil - - return nil -} - -func mapListResponse(resp *serviceaccount.AccessTokenMetadata, model *Model) error { - if resp == nil { - return fmt.Errorf("response input is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - if resp.Id == nil { - return fmt.Errorf("service account id not present") - } - - var createdAt basetypes.StringValue - if resp.CreatedAt != nil { - createdAtValue := *resp.CreatedAt - createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) - } - - var validUntil basetypes.StringValue - if resp.ValidUntil != nil { - validUntilValue := *resp.ValidUntil - validUntil = types.StringValue(validUntilValue.Format(time.RFC3339)) - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.ServiceAccountEmail.ValueString(), *resp.Id) - model.AccessTokenId = types.StringPointerValue(resp.Id) - model.CreatedAt = createdAt - model.ValidUntil = validUntil - - return nil -} diff --git a/stackit/internal/services/serviceaccount/token/resource_test.go b/stackit/internal/services/serviceaccount/token/resource_test.go deleted file mode 100644 index 08f7d26a..00000000 --- a/stackit/internal/services/serviceaccount/token/resource_test.go +++ /dev/null @@ -1,230 +0,0 @@ -package token - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" -) - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles []string - expected *serviceaccount.CreateAccessTokenPayload - isValid bool - }{ - { - "default_values", - &Model{ - TtlDays: types.Int64Value(20), - }, - []string{}, - &serviceaccount.CreateAccessTokenPayload{ - TtlDays: types.Int64Value(20).ValueInt64Pointer(), - }, - true, - }, - { - "nil_model", - nil, - []string{}, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapCreateResponse(t *testing.T) { - tests := []struct { - description string - input *serviceaccount.AccessToken - expected Model - isValid bool - }{ - { - "default_values", - &serviceaccount.AccessToken{ - Id: utils.Ptr("aid"), - Token: utils.Ptr("token"), - }, - Model{ - Id: types.StringValue("pid,email,aid"), - ProjectId: types.StringValue("pid"), - ServiceAccountEmail: types.StringValue("email"), - Token: types.StringValue("token"), - AccessTokenId: types.StringValue("aid"), - RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - true, - }, - { - "complete_values", - &serviceaccount.AccessToken{ - Id: utils.Ptr("aid"), - Token: utils.Ptr("token"), - CreatedAt: utils.Ptr(time.Now()), - ValidUntil: utils.Ptr(time.Now().Add(24 * time.Hour)), - Active: utils.Ptr(true), - }, - Model{ - Id: types.StringValue("pid,email,aid"), - ProjectId: types.StringValue("pid"), - ServiceAccountEmail: types.StringValue("email"), - Token: types.StringValue("token"), - AccessTokenId: types.StringValue("aid"), - Active: types.BoolValue(true), - CreatedAt: types.StringValue(time.Now().Format(time.RFC3339)), - ValidUntil: types.StringValue(time.Now().Add(24 * time.Hour).Format(time.RFC3339)), - RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "nil_response_2", - &serviceaccount.AccessToken{}, - Model{}, - false, - }, - { - "no_id", - &serviceaccount.AccessToken{ - Token: utils.Ptr("token"), - }, - Model{}, - false, - }, - { - "no_token", - &serviceaccount.AccessToken{ - Id: utils.Ptr("id"), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - ServiceAccountEmail: tt.expected.ServiceAccountEmail, - RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), - } - err := mapCreateResponse(tt.input, model) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(*model, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapListResponse(t *testing.T) { - tests := []struct { - description string - input *serviceaccount.AccessTokenMetadata - expected Model - isValid bool - }{ - { - "valid_fields", - &serviceaccount.AccessTokenMetadata{ - Id: utils.Ptr("aid"), - CreatedAt: utils.Ptr(time.Now()), - ValidUntil: utils.Ptr(time.Now().Add(24 * time.Hour)), - }, - Model{ - Id: types.StringValue("pid,email,aid"), - ProjectId: types.StringValue("pid"), - ServiceAccountEmail: types.StringValue("email"), - AccessTokenId: types.StringValue("aid"), - CreatedAt: types.StringValue(time.Now().Format(time.RFC3339)), // Adjusted for test setup time - ValidUntil: types.StringValue(time.Now().Add(24 * time.Hour).Format(time.RFC3339)), // Adjust for format - RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "nil_fields", - &serviceaccount.AccessTokenMetadata{ - Id: nil, - }, - Model{}, - false, - }, - { - "no_id", - &serviceaccount.AccessTokenMetadata{ - CreatedAt: utils.Ptr(time.Now()), - ValidUntil: utils.Ptr(time.Now().Add(24 * time.Hour)), - }, - Model{}, - false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - model := &Model{ - ProjectId: tt.expected.ProjectId, - ServiceAccountEmail: tt.expected.ServiceAccountEmail, - RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), - } - err := mapListResponse(tt.input, model) - if !tt.isValid && err == nil { - t.Fatalf("Expected an error but did not get one") - } - if tt.isValid && err != nil { - t.Fatalf("Did not expect an error but got one: %v", err) - } - if tt.isValid { - diff := cmp.Diff(*model, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/serviceaccount/utils/util.go b/stackit/internal/services/serviceaccount/utils/util.go deleted file mode 100644 index 5fd45eb0..00000000 --- a/stackit/internal/services/serviceaccount/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *serviceaccount.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ServiceAccountCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ServiceAccountCustomEndpoint)) - } - apiClient, err := serviceaccount.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/serviceaccount/utils/util_test.go b/stackit/internal/services/serviceaccount/utils/util_test.go deleted file mode 100644 index e18942f7..00000000 --- a/stackit/internal/services/serviceaccount/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://serviceaccount-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *serviceaccount.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *serviceaccount.APIClient { - apiClient, err := serviceaccount.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ServiceAccountCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *serviceaccount.APIClient { - apiClient, err := serviceaccount.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/serviceenablement/utils/util.go b/stackit/internal/services/serviceenablement/utils/util.go deleted file mode 100644 index 77cdd689..00000000 --- a/stackit/internal/services/serviceenablement/utils/util.go +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *serviceenablement.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.ServiceEnablementCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ServiceEnablementCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := serviceenablement.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/serviceenablement/utils/util_test.go b/stackit/internal/services/serviceenablement/utils/util_test.go deleted file mode 100644 index 5825ed6f..00000000 --- a/stackit/internal/services/serviceenablement/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://serviceenablement-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *serviceenablement.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *serviceenablement.APIClient { - apiClient, err := serviceenablement.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithRegion("eu01"), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - ServiceEnablementCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *serviceenablement.APIClient { - apiClient, err := serviceenablement.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/ske/cluster/datasource.go b/stackit/internal/services/ske/cluster/datasource.go deleted file mode 100644 index 2a7feca9..00000000 --- a/stackit/internal/services/ske/cluster/datasource.go +++ /dev/null @@ -1,372 +0,0 @@ -package ske - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &clusterDataSource{} -) - -// NewClusterDataSource is a helper function to simplify the provider implementation. -func NewClusterDataSource() datasource.DataSource { - return &clusterDataSource{} -} - -// clusterDataSource is the data source implementation. -type clusterDataSource struct { - client *ske.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *clusterDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_ske_cluster" -} - -// Configure adds the provider configured client to the data source. -func (r *clusterDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := skeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "SKE client configured") -} -func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - Description: "SKE Cluster data source schema. Must have a `region` specified in the provider configuration.", - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal data source. ID. It is structured as \"`project_id`,`name`\".", - Computed: true, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the cluster is associated.", - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The cluster name.", - Required: true, - }, - "kubernetes_version_min": schema.StringAttribute{ - Description: `The minimum Kubernetes version, this field is always nil. ` + SKEUpdateDoc + " To get the current kubernetes version being used for your cluster, use the `kubernetes_version_used` field.", - Computed: true, - }, - "kubernetes_version_used": schema.StringAttribute{ - Description: "Full Kubernetes version used. For example, if `1.22` was selected, this value may result to `1.22.15`", - Computed: true, - }, - "egress_address_ranges": schema.ListAttribute{ - Description: "The outgoing network ranges (in CIDR notation) of traffic originating from workload on the cluster.", - Computed: true, - ElementType: types.StringType, - }, - "pod_address_ranges": schema.ListAttribute{ - Description: "The network ranges (in CIDR notation) used by pods of the cluster.", - Computed: true, - ElementType: types.StringType, - }, - "node_pools": schema.ListNestedAttribute{ - Description: "One or more `node_pool` block as defined below.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "Specifies the name of the node pool.", - Computed: true, - }, - "machine_type": schema.StringAttribute{ - Description: "The machine type.", - Computed: true, - }, - "os_name": schema.StringAttribute{ - Description: "The name of the OS image.", - Computed: true, - }, - "os_version_min": schema.StringAttribute{ - Description: "The minimum OS image version, this field is always nil. " + SKEUpdateDoc + " To get the current OS image version being used for the node pool, use the read-only `os_version_used` field.", - Computed: true, - }, - "os_version": schema.StringAttribute{ - Description: "The OS image version.", - Computed: true, - }, - "os_version_used": schema.StringAttribute{ - Description: "Full OS image version used. For example, if 3815.2 was set in `os_version_min`, this value may result to 3815.2.2. " + SKEUpdateDoc, - Computed: true, - }, - "minimum": schema.Int64Attribute{ - Description: "Minimum number of nodes in the pool.", - Computed: true, - }, - - "maximum": schema.Int64Attribute{ - Description: "Maximum number of nodes in the pool.", - Computed: true, - }, - - "max_surge": schema.Int64Attribute{ - Description: "The maximum number of nodes upgraded simultaneously.", - Computed: true, - }, - "max_unavailable": schema.Int64Attribute{ - Description: "The maximum number of nodes unavailable during upgraded.", - Computed: true, - }, - "volume_type": schema.StringAttribute{ - Description: "Specifies the volume type.", - Computed: true, - }, - "volume_size": schema.Int64Attribute{ - Description: "The volume size in GB.", - Computed: true, - }, - "labels": schema.MapAttribute{ - Description: "Labels to add to each node.", - Computed: true, - ElementType: types.StringType, - }, - "taints": schema.ListNestedAttribute{ - Description: "Specifies a taint list as defined below.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "effect": schema.StringAttribute{ - Description: "The taint effect.", - Computed: true, - }, - "key": schema.StringAttribute{ - Description: "Taint key to be applied to a node.", - Computed: true, - }, - "value": schema.StringAttribute{ - Description: "Taint value corresponding to the taint key.", - Computed: true, - }, - }, - }, - }, - "cri": schema.StringAttribute{ - Description: "Specifies the container runtime.", - Computed: true, - }, - "availability_zones": schema.ListAttribute{ - Description: "Specify a list of availability zones.", - ElementType: types.StringType, - Computed: true, - }, - "allow_system_components": schema.BoolAttribute{ - Description: "Allow system components to run on this node pool.", - Computed: true, - }, - }, - }, - }, - "maintenance": schema.SingleNestedAttribute{ - Description: "A single maintenance block as defined below", - Computed: true, - Attributes: map[string]schema.Attribute{ - "enable_kubernetes_version_updates": schema.BoolAttribute{ - Description: "Flag to enable/disable auto-updates of the Kubernetes version.", - Computed: true, - }, - "enable_machine_image_version_updates": schema.BoolAttribute{ - Description: "Flag to enable/disable auto-updates of the OS image version.", - Computed: true, - }, - "start": schema.StringAttribute{ - Description: "Date time for maintenance window start.", - Computed: true, - }, - "end": schema.StringAttribute{ - Description: "Date time for maintenance window end.", - Computed: true, - }, - }, - }, - - "network": schema.SingleNestedAttribute{ - Description: "Network block as defined below.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "ID of the STACKIT Network Area (SNA) network into which the cluster will be deployed.", - Computed: true, - Validators: []validator.String{ - validate.UUID(), - }, - }, - }, - }, - - "hibernations": schema.ListNestedAttribute{ - Description: "One or more hibernation block as defined below.", - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "start": schema.StringAttribute{ - Description: "Start time of cluster hibernation in crontab syntax.", - Computed: true, - }, - "end": schema.StringAttribute{ - Description: "End time of hibernation, in crontab syntax.", - Computed: true, - }, - "timezone": schema.StringAttribute{ - Description: "Timezone name corresponding to a file in the IANA Time Zone database.", - Computed: true, - }, - }, - }, - }, - - "extensions": schema.SingleNestedAttribute{ - Description: "A single extensions block as defined below", - Computed: true, - Attributes: map[string]schema.Attribute{ - "argus": schema.SingleNestedAttribute{ - Description: "A single argus block as defined below. This field is deprecated and will be removed 06 January 2026.", - DeprecationMessage: "Use observability instead.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Flag to enable/disable argus extensions.", - Computed: true, - }, - "argus_instance_id": schema.StringAttribute{ - Description: "Instance ID of argus", - Computed: true, - }, - }, - }, - "observability": schema.SingleNestedAttribute{ - Description: "A single observability block as defined below.", - Computed: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Flag to enable/disable Observability extensions.", - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: "Observability instance ID to choose which Observability instance is used. Required when enabled is set to `true`.", - Computed: true, - }, - }, - }, - "acl": schema.SingleNestedAttribute{ - Description: "Cluster access control configuration", - Computed: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Is ACL enabled?", - Computed: true, - }, - "allowed_cidrs": schema.ListAttribute{ - Description: "Specify a list of CIDRs to whitelist", - Computed: true, - ElementType: types.StringType, - }, - }, - }, - "dns": schema.SingleNestedAttribute{ - Description: "DNS extension configuration", - Computed: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Flag to enable/disable DNS extensions", - Computed: true, - }, - "zones": schema.ListAttribute{ - Description: "Specify a list of domain filters for externalDNS (e.g., `foo.runs.onstackit.cloud`)", - Computed: true, - ElementType: types.StringType, - }, - }, - }, - }, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: "The resource region. If not defined, the provider region is used.", - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.Config.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := state.ProjectId.ValueString() - name := state.Name.ValueString() - region := r.providerData.GetRegionWithOverride(state.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "region", region) - clusterResp, err := r.client.GetCluster(ctx, projectId, region, name).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading cluster", - fmt.Sprintf("Cluster with name %q does not exist in project %q.", name, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, clusterResp, &state, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SKE cluster read") -} diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go deleted file mode 100644 index eaf35e90..00000000 --- a/stackit/internal/services/ske/cluster/resource.go +++ /dev/null @@ -1,2292 +0,0 @@ -package ske - -import ( - "context" - "fmt" - "net/http" - "regexp" - "sort" - "strings" - "time" - - serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" - skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "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/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" - enablementWait "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/wait" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - skeWait "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "golang.org/x/mod/semver" -) - -const ( - DefaultOSName = "flatcar" - DefaultCRI = "containerd" - DefaultVolumeType = "storage_premium_perf1" - DefaultVolumeSizeGB int64 = 20 - VersionStateSupported = "supported" - VersionStatePreview = "preview" - VersionStateDeprecated = "deprecated" - - SKEUpdateDoc = "SKE automatically updates the cluster Kubernetes version if you have set `maintenance.enable_kubernetes_version_updates` to true or if there is a mandatory update, as described in [General information for Kubernetes & OS updates](https://docs.stackit.cloud/products/runtime/kubernetes-engine/basics/version-updates/)." -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &clusterResource{} - _ resource.ResourceWithConfigure = &clusterResource{} - _ resource.ResourceWithImportState = &clusterResource{} - _ resource.ResourceWithModifyPlan = &clusterResource{} -) - -type skeClient interface { - GetClusterExecute(ctx context.Context, projectId, region, clusterName string) (*ske.Cluster, error) -} - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - KubernetesVersionMin types.String `tfsdk:"kubernetes_version_min"` - KubernetesVersionUsed types.String `tfsdk:"kubernetes_version_used"` - NodePools types.List `tfsdk:"node_pools"` - Maintenance types.Object `tfsdk:"maintenance"` - Network types.Object `tfsdk:"network"` - Hibernations types.List `tfsdk:"hibernations"` - Extensions types.Object `tfsdk:"extensions"` - EgressAddressRanges types.List `tfsdk:"egress_address_ranges"` - PodAddressRanges types.List `tfsdk:"pod_address_ranges"` - Region types.String `tfsdk:"region"` -} - -// Struct corresponding to Model.NodePools[i] -type nodePool struct { - Name types.String `tfsdk:"name"` - MachineType types.String `tfsdk:"machine_type"` - OSName types.String `tfsdk:"os_name"` - OSVersionMin types.String `tfsdk:"os_version_min"` - OSVersion types.String `tfsdk:"os_version"` - OSVersionUsed types.String `tfsdk:"os_version_used"` - Minimum types.Int64 `tfsdk:"minimum"` - Maximum types.Int64 `tfsdk:"maximum"` - MaxSurge types.Int64 `tfsdk:"max_surge"` - MaxUnavailable types.Int64 `tfsdk:"max_unavailable"` - VolumeType types.String `tfsdk:"volume_type"` - VolumeSize types.Int64 `tfsdk:"volume_size"` - Labels types.Map `tfsdk:"labels"` - Taints types.List `tfsdk:"taints"` - CRI types.String `tfsdk:"cri"` - AvailabilityZones types.List `tfsdk:"availability_zones"` - AllowSystemComponents types.Bool `tfsdk:"allow_system_components"` -} - -// Types corresponding to nodePool -var nodePoolTypes = map[string]attr.Type{ - "name": basetypes.StringType{}, - "machine_type": basetypes.StringType{}, - "os_name": basetypes.StringType{}, - "os_version_min": basetypes.StringType{}, - "os_version": basetypes.StringType{}, - "os_version_used": basetypes.StringType{}, - "minimum": basetypes.Int64Type{}, - "maximum": basetypes.Int64Type{}, - "max_surge": basetypes.Int64Type{}, - "max_unavailable": basetypes.Int64Type{}, - "volume_type": basetypes.StringType{}, - "volume_size": basetypes.Int64Type{}, - "labels": basetypes.MapType{ElemType: types.StringType}, - "taints": basetypes.ListType{ElemType: types.ObjectType{AttrTypes: taintTypes}}, - "cri": basetypes.StringType{}, - "availability_zones": basetypes.ListType{ElemType: types.StringType}, - "allow_system_components": basetypes.BoolType{}, -} - -// Struct corresponding to nodePool.Taints[i] -type taint struct { - Effect types.String `tfsdk:"effect"` - Key types.String `tfsdk:"key"` - Value types.String `tfsdk:"value"` -} - -// Types corresponding to taint -var taintTypes = map[string]attr.Type{ - "effect": basetypes.StringType{}, - "key": basetypes.StringType{}, - "value": basetypes.StringType{}, -} - -// Struct corresponding to Model.maintenance -type maintenance struct { - EnableKubernetesVersionUpdates types.Bool `tfsdk:"enable_kubernetes_version_updates"` - EnableMachineImageVersionUpdates types.Bool `tfsdk:"enable_machine_image_version_updates"` - Start types.String `tfsdk:"start"` - End types.String `tfsdk:"end"` -} - -// Types corresponding to maintenance -var maintenanceTypes = map[string]attr.Type{ - "enable_kubernetes_version_updates": basetypes.BoolType{}, - "enable_machine_image_version_updates": basetypes.BoolType{}, - "start": basetypes.StringType{}, - "end": basetypes.StringType{}, -} - -// Struct corresponding to Model.Network -type network struct { - ID types.String `tfsdk:"id"` -} - -// Types corresponding to network -var networkTypes = map[string]attr.Type{ - "id": basetypes.StringType{}, -} - -// Struct corresponding to Model.Hibernations[i] -type hibernation struct { - Start types.String `tfsdk:"start"` - End types.String `tfsdk:"end"` - Timezone types.String `tfsdk:"timezone"` -} - -// Types corresponding to hibernation -var hibernationTypes = map[string]attr.Type{ - "start": basetypes.StringType{}, - "end": basetypes.StringType{}, - "timezone": basetypes.StringType{}, -} - -// Struct corresponding to Model.Extensions -type extensions struct { - Argus types.Object `tfsdk:"argus"` - Observability types.Object `tfsdk:"observability"` - ACL types.Object `tfsdk:"acl"` - DNS types.Object `tfsdk:"dns"` -} - -// Types corresponding to extensions -var extensionsTypes = map[string]attr.Type{ - "argus": basetypes.ObjectType{AttrTypes: argusTypes}, - "observability": basetypes.ObjectType{AttrTypes: observabilityTypes}, - "acl": basetypes.ObjectType{AttrTypes: aclTypes}, - "dns": basetypes.ObjectType{AttrTypes: dnsTypes}, -} - -// Struct corresponding to extensions.ACL -type acl struct { - Enabled types.Bool `tfsdk:"enabled"` - AllowedCIDRs types.List `tfsdk:"allowed_cidrs"` -} - -// Types corresponding to acl -var aclTypes = map[string]attr.Type{ - "enabled": basetypes.BoolType{}, - "allowed_cidrs": basetypes.ListType{ElemType: types.StringType}, -} - -// Struct corresponding to extensions.Argus -type argus struct { - Enabled types.Bool `tfsdk:"enabled"` - ArgusInstanceId types.String `tfsdk:"argus_instance_id"` -} - -// Types corresponding to argus -var argusTypes = map[string]attr.Type{ - "enabled": basetypes.BoolType{}, - "argus_instance_id": basetypes.StringType{}, -} - -// Struct corresponding to extensions.Observability -type observability struct { - Enabled types.Bool `tfsdk:"enabled"` - InstanceId types.String `tfsdk:"instance_id"` -} - -// Types corresponding to observability -var observabilityTypes = map[string]attr.Type{ - "enabled": basetypes.BoolType{}, - "instance_id": basetypes.StringType{}, -} - -// Struct corresponding to extensions.DNS -type dns struct { - Enabled types.Bool `tfsdk:"enabled"` - Zones types.List `tfsdk:"zones"` -} - -// Types corresponding to DNS -var dnsTypes = map[string]attr.Type{ - "enabled": basetypes.BoolType{}, - "zones": basetypes.ListType{ElemType: types.StringType}, -} - -// NewClusterResource is a helper function to simplify the provider implementation. -func NewClusterResource() resource.Resource { - return &clusterResource{} -} - -// clusterResource is the resource implementation. -type clusterResource struct { - skeClient *ske.APIClient - enablementClient *serviceenablement.APIClient - providerData core.ProviderData -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *clusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Metadata returns the resource type name. -func (r *clusterResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_ske_cluster" -} - -// Configure adds the provider configured client to the resource. -func (r *clusterResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - skeClient := skeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - serviceEnablementClient := serviceenablementUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.skeClient = skeClient - r.enablementClient = serviceEnablementClient - tflog.Info(ctx, "SKE cluster clients configured") -} - -// Schema defines the schema for the resource. -func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SKE Cluster Resource schema. Must have a `region` specified in the provider configuration.", - "node_pools_plan_note": "When updating `node_pools` of a `stackit_ske_cluster`, the Terraform plan might appear incorrect as it matches the node pools by index rather than by name. " + - "However, the SKE API correctly identifies node pools by name and applies the intended changes. Please review your changes carefully to ensure the correct configuration will be applied.", - "max_surge": "Maximum number of additional VMs that are created during an update.", - "max_unavailable": "Maximum number of VMs that that can be unavailable during an update.", - "nodepool_validators": "If set (larger than 0), then it must be at least the amount of zones configured for the nodepool. The `max_surge` and `max_unavailable` fields cannot both be unset at the same time.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: fmt.Sprintf("%s\n%s", descriptions["main"], descriptions["node_pools_plan_note"]), - // Callout block: https://developer.hashicorp.com/terraform/registry/providers/docs#callouts - MarkdownDescription: fmt.Sprintf("%s\n\n-> %s", descriptions["main"], descriptions["node_pools_plan_note"]), - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`name`\".", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "project_id": schema.StringAttribute{ - Description: "STACKIT project ID to which the cluster is associated.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: "The cluster name.", - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "kubernetes_version_min": schema.StringAttribute{ - Description: "The minimum Kubernetes version. This field will be used to set the minimum kubernetes version on creation/update of the cluster. If unset, the latest supported Kubernetes version will be used. " + SKEUpdateDoc + " To get the current kubernetes version being used for your cluster, use the read-only `kubernetes_version_used` field.", - Optional: true, - Validators: []validator.String{ - validate.VersionNumber(), - }, - }, - "kubernetes_version_used": schema.StringAttribute{ - Description: "Full Kubernetes version used. For example, if 1.22 was set in `kubernetes_version_min`, this value may result to 1.22.15. " + SKEUpdateDoc, - Computed: true, - PlanModifiers: []planmodifier.String{ - utils.UseStateForUnknownIf(hasKubernetesMinChanged, "sets `UseStateForUnknown` only if `kubernetes_min_version` has not changed"), - }, - }, - "egress_address_ranges": schema.ListAttribute{ - Description: "The outgoing network ranges (in CIDR notation) of traffic originating from workload on the cluster.", - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "pod_address_ranges": schema.ListAttribute{ - Description: "The network ranges (in CIDR notation) used by pods of the cluster.", - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "node_pools": schema.ListNestedAttribute{ - Description: "One or more `node_pool` block as defined below.", - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "Specifies the name of the node pool.", - Required: true, - }, - "machine_type": schema.StringAttribute{ - Description: "The machine type.", - Required: true, - }, - "availability_zones": schema.ListAttribute{ - Description: "Specify a list of availability zones. E.g. `eu01-m`", - Required: true, - ElementType: types.StringType, - }, - "allow_system_components": schema.BoolAttribute{ - Description: "Allow system components to run on this node pool.", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - }, - "minimum": schema.Int64Attribute{ - Description: "Minimum number of nodes in the pool.", - Required: true, - }, - "maximum": schema.Int64Attribute{ - Description: "Maximum number of nodes in the pool.", - Required: true, - }, - "max_surge": schema.Int64Attribute{ - Description: fmt.Sprintf("%s %s", descriptions["max_surge"], descriptions["nodepool_validators"]), - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "max_unavailable": schema.Int64Attribute{ - Description: fmt.Sprintf("%s %s", descriptions["max_unavailable"], descriptions["nodepool_validators"]), - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "os_name": schema.StringAttribute{ - Description: "The name of the OS image. Defaults to `flatcar`.", - Optional: true, - Computed: true, - Default: stringdefault.StaticString(DefaultOSName), - }, - "os_version_min": schema.StringAttribute{ - Description: "The minimum OS image version. This field will be used to set the minimum OS image version on creation/update of the cluster. If unset, the latest supported OS image version will be used. " + SKEUpdateDoc + " To get the current OS image version being used for the node pool, use the read-only `os_version_used` field.", - Optional: true, - Validators: []validator.String{ - validate.VersionNumber(), - }, - }, - "os_version": schema.StringAttribute{ - Description: "This field is deprecated, use `os_version_min` to configure the version and `os_version_used` to get the currently used version instead.", - DeprecationMessage: "Use `os_version_min` to configure the version and `os_version_used` to get the currently used version instead. Setting a specific OS image version will cause errors during minor OS upgrades due to forced updates.", - Optional: true, - }, - "os_version_used": schema.StringAttribute{ - Description: "Full OS image version used. For example, if 3815.2 was set in `os_version_min`, this value may result to 3815.2.2. " + SKEUpdateDoc, - Computed: true, - PlanModifiers: []planmodifier.String{ - utils.UseStateForUnknownIf(hasOsVersionMinChanged, "sets `UseStateForUnknown` only if `os_version_min` has not changed"), - }, - }, - "volume_type": schema.StringAttribute{ - Description: "Specifies the volume type. Defaults to `storage_premium_perf1`.", - Optional: true, - Computed: true, - Default: stringdefault.StaticString(DefaultVolumeType), - }, - "volume_size": schema.Int64Attribute{ - Description: "The volume size in GB. Defaults to `20`", - Optional: true, - Computed: true, - Default: int64default.StaticInt64(DefaultVolumeSizeGB), - }, - "labels": schema.MapAttribute{ - Description: "Labels to add to each node.", - Optional: true, - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.Map{ - mapplanmodifier.UseStateForUnknown(), - }, - }, - "taints": schema.ListNestedAttribute{ - Description: "Specifies a taint list as defined below.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "effect": schema.StringAttribute{ - Description: "The taint effect. E.g `PreferNoSchedule`.", - Required: true, - }, - "key": schema.StringAttribute{ - Description: "Taint key to be applied to a node.", - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - }, - }, - "value": schema.StringAttribute{ - Description: "Taint value corresponding to the taint key.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - }, - }, - "cri": schema.StringAttribute{ - Description: "Specifies the container runtime. Defaults to `containerd`", - Optional: true, - Computed: true, - Default: stringdefault.StaticString(DefaultCRI), - }, - }, - }, - }, - "maintenance": schema.SingleNestedAttribute{ - Description: "A single maintenance block as defined below.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "enable_kubernetes_version_updates": schema.BoolAttribute{ - Description: "Flag to enable/disable auto-updates of the Kubernetes version. Defaults to `true`. " + SKEUpdateDoc, - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - }, - "enable_machine_image_version_updates": schema.BoolAttribute{ - Description: "Flag to enable/disable auto-updates of the OS image version. Defaults to `true`. " + SKEUpdateDoc, - Optional: true, - Computed: true, - Default: booldefault.StaticBool(true), - }, - "start": schema.StringAttribute{ - Description: "Time for maintenance window start. E.g. `01:23:45Z`, `05:00:00+02:00`.", - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile(`^(((\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$`), - "must be a full-time as defined by RFC3339, Section 5.6. E.g. `01:23:45Z`, `05:00:00+02:00`", - ), - }, - }, - "end": schema.StringAttribute{ - Description: "Time for maintenance window end. E.g. `01:23:45Z`, `05:00:00+02:00`.", - Required: true, - Validators: []validator.String{ - stringvalidator.RegexMatches( - regexp.MustCompile(`^(((\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$`), - "must be a full-time as defined by RFC3339, Section 5.6. E.g. `01:23:45Z`, `05:00:00+02:00`", - ), - }, - }, - }, - }, - "network": schema.SingleNestedAttribute{ - Description: "Network block as defined below.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: "ID of the STACKIT Network Area (SNA) network into which the cluster will be deployed.", - Optional: true, - Validators: []validator.String{ - validate.UUID(), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - }, - "hibernations": schema.ListNestedAttribute{ - Description: "One or more hibernation block as defined below.", - Optional: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "start": schema.StringAttribute{ - Description: "Start time of cluster hibernation in crontab syntax. E.g. `0 18 * * *` for starting everyday at 6pm.", - Required: true, - }, - "end": schema.StringAttribute{ - Description: "End time of hibernation in crontab syntax. E.g. `0 8 * * *` for waking up the cluster at 8am.", - Required: true, - }, - "timezone": schema.StringAttribute{ - Description: "Timezone name corresponding to a file in the IANA Time Zone database. i.e. `Europe/Berlin`.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, - }, - }, - "extensions": schema.SingleNestedAttribute{ - Description: "A single extensions block as defined below.", - Optional: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "argus": schema.SingleNestedAttribute{ - Description: "A single argus block as defined below. This field is deprecated and will be removed 06 January 2026.", - DeprecationMessage: "Use observability instead.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Flag to enable/disable Argus extensions.", - Required: true, - }, - "argus_instance_id": schema.StringAttribute{ - Description: "Argus instance ID to choose which Argus instance is used. Required when enabled is set to `true`.", - Optional: true, - }, - }, - }, - "observability": schema.SingleNestedAttribute{ - Description: "A single observability block as defined below.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Flag to enable/disable Observability extensions.", - Required: true, - }, - "instance_id": schema.StringAttribute{ - Description: "Observability instance ID to choose which Observability instance is used. Required when enabled is set to `true`.", - Optional: true, - }, - }, - }, - "acl": schema.SingleNestedAttribute{ - Description: "Cluster access control configuration.", - Optional: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Is ACL enabled?", - Required: true, - }, - "allowed_cidrs": schema.ListAttribute{ - Description: "Specify a list of CIDRs to whitelist.", - Required: true, - ElementType: types.StringType, - }, - }, - }, - "dns": schema.SingleNestedAttribute{ - Description: "DNS extension configuration", - Optional: true, - Attributes: map[string]schema.Attribute{ - "enabled": schema.BoolAttribute{ - Description: "Flag to enable/disable DNS extensions", - Required: true, - }, - "zones": schema.ListAttribute{ - Description: "Specify a list of domain filters for externalDNS (e.g., `foo.runs.onstackit.cloud`)", - Optional: true, - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.List{ - listvalidator.ValueStringsAre(validate.NoUUID()), - }, - }, - }, - }, - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// The argus extension is deprecated but can still be used until it is removed on 06 January 2026. -func (r *clusterResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { - var resourceModel Model - resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) - if resp.Diagnostics.HasError() { - return - } - - // function is used in order to be able to write easier unit tests - validateConfig(ctx, &resp.Diagnostics, &resourceModel) -} - -func validateConfig(ctx context.Context, respDiags *diag.Diagnostics, model *Model) { - // If no extensions are configured, return without error. - if utils.IsUndefined(model.Extensions) { - return - } - - extensions := &extensions{} - diags := model.Extensions.As(ctx, extensions, basetypes.ObjectAsOptions{}) - respDiags.Append(diags...) - if respDiags.HasError() { - return - } - - if !utils.IsUndefined(extensions.Argus) && !utils.IsUndefined(extensions.Observability) { - core.LogAndAddError(ctx, respDiags, "Error configuring cluster", "You cannot provide both the `argus` and `observability` extension fields simultaneously. Please remove the deprecated `argus` field, and use `observability`.") - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - clusterName := model.Name.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", clusterName) - ctx = tflog.SetField(ctx, "region", region) - - // If SKE functionality is not enabled, enable it - err := r.enablementClient.EnableServiceRegional(ctx, region, projectId, utils.SKEServiceId).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Calling API to enable SKE: %v", err)) - return - } - - _, err = enablementWait.EnableServiceWaitHandler(ctx, r.enablementClient, region, projectId, utils.SKEServiceId).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Wait for SKE enablement: %v", err)) - return - } - - availableKubernetesVersions, availableMachines, err := r.loadAvailableVersions(ctx, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating cluster", fmt.Sprintf("Loading available Kubernetes and machine image versions: %v", err)) - return - } - - r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableKubernetesVersions, availableMachines, nil, nil) - if resp.Diagnostics.HasError() { - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - tflog.Info(ctx, "SKE cluster created") -} - -func sortK8sVersions(versions []ske.KubernetesVersion) { - sort.Slice(versions, func(i, j int) bool { - v1, v2 := (versions)[i].Version, (versions)[j].Version - if v1 == nil { - return false - } - if v2 == nil { - return true - } - - // we have to make copies of the input strings to add prefixes, - // otherwise we would be changing the passed elements - t1, t2 := *v1, *v2 - - if !strings.HasPrefix(t1, "v") { - t1 = "v" + t1 - } - if !strings.HasPrefix(t2, "v") { - t2 = "v" + t2 - } - return semver.Compare(t1, t2) > 0 - }) -} - -// loadAvailableVersions loads the available k8s and machine versions from the API. -// The k8s versions are sorted descending order, i.e. the latest versions (including previews) -// are listed first -func (r *clusterResource) loadAvailableVersions(ctx context.Context, region string) ([]ske.KubernetesVersion, []ske.MachineImage, error) { - c := r.skeClient - res, err := c.ListProviderOptions(ctx, region).Execute() - if err != nil { - return nil, nil, fmt.Errorf("calling API: %w", err) - } - - if res.KubernetesVersions == nil { - return nil, nil, fmt.Errorf("API response has nil kubernetesVersions") - } - - if res.MachineImages == nil { - return nil, nil, fmt.Errorf("API response has nil machine images") - } - - return *res.KubernetesVersions, *res.MachineImages, nil -} - -// getCurrentVersions makes a call to get the details of a cluster and returns the current kubernetes version and a -// a map with the machine image for each nodepool, which can be used to check the current machine image versions. -// if the cluster doesn't exist or some error occurs, returns nil for both -func getCurrentVersions(ctx context.Context, c skeClient, m *Model) (kubernetesVersion *string, nodePoolMachineImages map[string]*ske.Image) { - res, err := c.GetClusterExecute(ctx, m.ProjectId.ValueString(), m.Region.ValueString(), m.Name.ValueString()) - if err != nil || res == nil { - return nil, nil - } - - if res.Kubernetes != nil { - kubernetesVersion = res.Kubernetes.Version - } - - if res.Nodepools == nil { - return kubernetesVersion, nil - } - - nodePoolMachineImages = map[string]*ske.Image{} - for _, nodePool := range *res.Nodepools { - if nodePool.Name == nil || nodePool.Machine == nil || nodePool.Machine.Image == nil || nodePool.Machine.Image.Name == nil { - continue - } - nodePoolMachineImages[*nodePool.Name] = nodePool.Machine.Image - } - - return kubernetesVersion, nodePoolMachineImages -} - -func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag.Diagnostics, model *Model, availableKubernetesVersions []ske.KubernetesVersion, availableMachineVersions []ske.MachineImage, currentKubernetesVersion *string, currentMachineImages map[string]*ske.Image) { - // cluster vars - projectId := model.ProjectId.ValueString() - name := model.Name.ValueString() - region := model.Region.ValueString() - kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableKubernetesVersions, currentKubernetesVersion, diags) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating cluster config API payload: %v", err)) - return - } - if hasDeprecatedVersion { - diags.AddWarning("Deprecated Kubernetes version", fmt.Sprintf("Version %s of Kubernetes is deprecated, please update it", *kubernetes.Version)) - } - nodePools, deprecatedVersionsUsed, err := toNodepoolsPayload(ctx, model, availableMachineVersions, currentMachineImages) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating node pools API payload: %v", err)) - return - } - if len(deprecatedVersionsUsed) != 0 { - diags.AddWarning("Deprecated node pools OS versions used", fmt.Sprintf("The following versions of machines are deprecated, please update them: [%s]", strings.Join(deprecatedVersionsUsed, ","))) - } - maintenance, err := toMaintenancePayload(ctx, model) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating maintenance API payload: %v", err)) - return - } - network, err := toNetworkPayload(ctx, model) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating network API payload: %v", err)) - return - } - hibernations, err := toHibernationsPayload(ctx, model) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating hibernations API payload: %v", err)) - return - } - extensions, err := toExtensionsPayload(ctx, model) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating extension API payload: %v", err)) - return - } - - payload := ske.CreateOrUpdateClusterPayload{ - Extensions: extensions, - Hibernation: hibernations, - Kubernetes: kubernetes, - Maintenance: maintenance, - Network: network, - Nodepools: &nodePools, - } - _, err = r.skeClient.CreateOrUpdateCluster(ctx, projectId, region, name).CreateOrUpdateClusterPayload(payload).Execute() - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // The passed context to createOrUpdateCluster will not be updated outside of this function. - // Call tflog.Info here, to log the information of the updated context - tflog.Info(ctx, "Triggered create/update cluster") - - waitResp, err := skeWait.CreateOrUpdateClusterWaitHandler(ctx, r.skeClient, projectId, region, name).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Cluster creation waiting: %v", err)) - return - } - if waitResp.Status.Error != nil && waitResp.Status.Error.Message != nil && *waitResp.Status.Error.Code == ske.RUNTIMEERRORCODE_OBSERVABILITY_INSTANCE_NOT_FOUND { - core.LogAndAddWarning(ctx, diags, "Warning during creating/updating cluster", fmt.Sprintf("Cluster is in Impaired state due to an invalid observability instance id, the cluster is usable but metrics won't be forwarded: %s", *waitResp.Status.Error.Message)) - } - - err = mapFields(ctx, waitResp, model, region) - if err != nil { - core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Processing API payload: %v", err)) - return - } -} - -func toNodepoolsPayload(ctx context.Context, m *Model, availableMachineVersions []ske.MachineImage, currentMachineImages map[string]*ske.Image) ([]ske.Nodepool, []string, error) { - nodePools := []nodePool{} - diags := m.NodePools.ElementsAs(ctx, &nodePools, false) - if diags.HasError() { - return nil, nil, core.DiagsToError(diags) - } - - cnps := []ske.Nodepool{} - deprecatedVersionsUsed := []string{} - for i := range nodePools { - nodePool := nodePools[i] - - name := conversion.StringValueToPointer(nodePool.Name) - if name == nil { - return nil, nil, fmt.Errorf("found nil node pool name for node_pool[%d]", i) - } - - // taints - taintsModel := []taint{} - diags := nodePool.Taints.ElementsAs(ctx, &taintsModel, false) - if diags.HasError() { - return nil, nil, core.DiagsToError(diags) - } - - ts := []ske.Taint{} - for _, v := range taintsModel { - t := ske.Taint{ - Effect: ske.TaintGetEffectAttributeType(conversion.StringValueToPointer(v.Effect)), - Key: conversion.StringValueToPointer(v.Key), - Value: conversion.StringValueToPointer(v.Value), - } - ts = append(ts, t) - } - - // labels - var ls *map[string]string - if nodePool.Labels.IsNull() { - ls = nil - } else { - lsm := map[string]string{} - for k, v := range nodePool.Labels.Elements() { - nv, err := conversion.ToString(ctx, v) - if err != nil { - lsm[k] = "" - continue - } - lsm[k] = nv - } - ls = &lsm - } - - // zones - zs := []string{} - for _, v := range nodePool.AvailabilityZones.Elements() { - if v.IsNull() || v.IsUnknown() { - continue - } - s, err := conversion.ToString(ctx, v) - if err != nil { - continue - } - zs = append(zs, s) - } - - cn := ske.CRI{ - Name: ske.CRIGetNameAttributeType(conversion.StringValueToPointer(nodePool.CRI)), - } - - providedVersionMin := conversion.StringValueToPointer(nodePool.OSVersionMin) - if !nodePool.OSVersion.IsNull() { - if providedVersionMin != nil { - return nil, nil, fmt.Errorf("both `os_version` and `os_version_min` are set for for node_pool %q. Please use `os_version_min` only, `os_version` is deprecated", *name) - } - // os_version field deprecation - // this if clause should be removed once os_version field is completely removed - // os_version field value is used as minimum os version - providedVersionMin = conversion.StringValueToPointer(nodePool.OSVersion) - } - - machineOSName := conversion.StringValueToPointer(nodePool.OSName) - if machineOSName == nil { - return nil, nil, fmt.Errorf("found nil machine name for node_pool %q", *name) - } - - currentMachineImage := currentMachineImages[*name] - - machineVersion, hasDeprecatedVersion, err := latestMatchingMachineVersion(availableMachineVersions, providedVersionMin, *machineOSName, currentMachineImage) - if err != nil { - return nil, nil, fmt.Errorf("getting latest matching machine image version: %w", err) - } - if hasDeprecatedVersion && machineVersion != nil { - deprecatedVersionsUsed = append(deprecatedVersionsUsed, *machineVersion) - } - - cnp := ske.Nodepool{ - Name: name, - Minimum: conversion.Int64ValueToPointer(nodePool.Minimum), - Maximum: conversion.Int64ValueToPointer(nodePool.Maximum), - MaxSurge: conversion.Int64ValueToPointer(nodePool.MaxSurge), - MaxUnavailable: conversion.Int64ValueToPointer(nodePool.MaxUnavailable), - Machine: &ske.Machine{ - Type: conversion.StringValueToPointer(nodePool.MachineType), - Image: &ske.Image{ - Name: machineOSName, - Version: machineVersion, - }, - }, - Volume: &ske.Volume{ - Type: conversion.StringValueToPointer(nodePool.VolumeType), - Size: conversion.Int64ValueToPointer(nodePool.VolumeSize), - }, - Taints: &ts, - Cri: &cn, - Labels: ls, - AvailabilityZones: &zs, - AllowSystemComponents: conversion.BoolValueToPointer(nodePool.AllowSystemComponents), - } - cnps = append(cnps, cnp) - } - - if err := verifySystemComponentsInNodePools(cnps); err != nil { - return nil, nil, err - } - - return cnps, deprecatedVersionsUsed, nil -} - -// verifySystemComponentsInNodePools checks if at least one node pool has the allow_system_components attribute set to true. -func verifySystemComponentsInNodePools(nodePools []ske.Nodepool) error { - for _, nodePool := range nodePools { - if nodePool.AllowSystemComponents != nil && *nodePool.AllowSystemComponents { - return nil // A node pool allowing system components was found - } - } - return fmt.Errorf("at least one node_pool must allow system components") -} - -// latestMatchingMachineVersion determines the latest machine image version for the create/update payload. -// It considers the available versions for the specified OS (OSName), the minimum version configured by the user, -// and the current version in the cluster. The function's behavior is as follows: -// -// 1. If the minimum version is not set: -// - Return the current version if it exists. -// - Otherwise, return the latest available version for the specified OS. -// -// 2. If the minimum version is set: -// - If the minimum version is a downgrade, use the current version instead. -// - If a patch is not specified for the minimum version, return the latest patch for that minor version. -// -// 3. For the selected version, check its state and return it, indicating if it is deprecated or not. -func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin *string, osName string, currentImage *ske.Image) (version *string, deprecated bool, err error) { - deprecated = false - - if availableImages == nil { - return nil, false, fmt.Errorf("nil available machine versions") - } - - var availableMachineVersions []ske.MachineImageVersion - for _, machine := range availableImages { - if machine.Name != nil && *machine.Name == osName && machine.Versions != nil { - availableMachineVersions = *machine.Versions - } - } - - if len(availableImages) == 0 { - return nil, false, fmt.Errorf("there are no available machine versions for the provided machine image name %s", osName) - } - - if versionMin == nil { - // Different machine OSes have different versions. - // If the current machine image is nil or the machine image name has been updated, - // retrieve the latest supported version. Otherwise, use the current machine version. - if currentImage == nil || currentImage.Name == nil || *currentImage.Name != osName { - latestVersion, err := getLatestSupportedMachineVersion(availableMachineVersions) - if err != nil { - return nil, false, fmt.Errorf("get latest supported machine image version: %w", err) - } - return latestVersion, false, nil - } - versionMin = currentImage.Version - } else if currentImage != nil && currentImage.Name != nil && *currentImage.Name == osName { - // If the os_version_min is set but is lower than the current version used in the cluster, - // retain the current version to avoid downgrading. - minimumVersion := "v" + *versionMin - currentVersion := "v" + *currentImage.Version - - if semver.Compare(minimumVersion, currentVersion) == -1 { - versionMin = currentImage.Version - } - } - - var fullVersion bool - versionExp := validate.FullVersionRegex - versionRegex := regexp.MustCompile(versionExp) - if versionRegex.MatchString(*versionMin) { - fullVersion = true - } - - providedVersionPrefixed := "v" + *versionMin - - if !semver.IsValid(providedVersionPrefixed) { - return nil, false, fmt.Errorf("provided version is invalid") - } - - var versionUsed *string - var state *string - var availableVersionsArray []string - // Get the higher available version that matches the major, minor and patch version provided by the user - for _, v := range availableMachineVersions { - if v.State == nil || v.Version == nil { - continue - } - availableVersionsArray = append(availableVersionsArray, *v.Version) - vPreffixed := "v" + *v.Version - - if fullVersion { - // [MAJOR].[MINOR].[PATCH] version provided, match available version - if semver.Compare(vPreffixed, providedVersionPrefixed) == 0 { - versionUsed = v.Version - state = v.State - break - } - } else { - // [MAJOR].[MINOR] version provided, get the latest patch version - if semver.MajorMinor(vPreffixed) == semver.MajorMinor(providedVersionPrefixed) && - (semver.Compare(vPreffixed, providedVersionPrefixed) == 1 || semver.Compare(vPreffixed, providedVersionPrefixed) == 0) && - (v.State != nil && *v.State != VersionStatePreview) { - versionUsed = v.Version - state = v.State - } - } - } - - if versionUsed != nil { - deprecated = strings.EqualFold(*state, VersionStateDeprecated) - } - - // Throwing error if we could not match the version with the available versions - if versionUsed == nil { - return nil, false, fmt.Errorf("provided version is not one of the available machine image versions, available versions are: %s", strings.Join(availableVersionsArray, ",")) - } - - return versionUsed, deprecated, nil -} - -func getLatestSupportedMachineVersion(versions []ske.MachineImageVersion) (*string, error) { - foundMachineVersion := false - var latestVersion *string - for i := range versions { - version := versions[i] - if *version.State != VersionStateSupported { - continue - } - if latestVersion != nil { - oldSemVer := fmt.Sprintf("v%s", *latestVersion) - newSemVer := fmt.Sprintf("v%s", *version.Version) - if semver.Compare(newSemVer, oldSemVer) != 1 { - continue - } - } - - foundMachineVersion = true - latestVersion = version.Version - } - if !foundMachineVersion { - return nil, fmt.Errorf("no supported machine version found") - } - return latestVersion, nil -} - -func toHibernationsPayload(ctx context.Context, m *Model) (*ske.Hibernation, error) { - hibernation := []hibernation{} - diags := m.Hibernations.ElementsAs(ctx, &hibernation, false) - if diags.HasError() { - return nil, core.DiagsToError(diags) - } - - if len(hibernation) == 0 { - return nil, nil - } - - scs := []ske.HibernationSchedule{} - for _, h := range hibernation { - sc := ske.HibernationSchedule{ - Start: conversion.StringValueToPointer(h.Start), - End: conversion.StringValueToPointer(h.End), - } - if !h.Timezone.IsNull() && !h.Timezone.IsUnknown() { - tz := h.Timezone.ValueString() - sc.Timezone = &tz - } - scs = append(scs, sc) - } - - return &ske.Hibernation{ - Schedules: &scs, - }, nil -} - -func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) { - if m.Extensions.IsNull() || m.Extensions.IsUnknown() { - return nil, nil - } - ex := extensions{} - diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting extensions object: %v", diags.Errors()) - } - - var skeAcl *ske.ACL - if !(ex.ACL.IsNull() || ex.ACL.IsUnknown()) { - acl := acl{} - diags = ex.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting extensions.acl object: %v", diags.Errors()) - } - aclEnabled := conversion.BoolValueToPointer(acl.Enabled) - - cidrs := []string{} - diags = acl.AllowedCIDRs.ElementsAs(ctx, &cidrs, true) - if diags.HasError() { - return nil, fmt.Errorf("converting extensions.acl.cidrs object: %v", diags.Errors()) - } - skeAcl = &ske.ACL{ - Enabled: aclEnabled, - AllowedCidrs: &cidrs, - } - } - - var skeObservability *ske.Observability - if !utils.IsUndefined(ex.Observability) { - observability := observability{} - diags = ex.Observability.As(ctx, &observability, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting extensions.observability object: %v", diags.Errors()) - } - observabilityEnabled := conversion.BoolValueToPointer(observability.Enabled) - observabilityInstanceId := conversion.StringValueToPointer(observability.InstanceId) - skeObservability = &ske.Observability{ - Enabled: observabilityEnabled, - InstanceId: observabilityInstanceId, - } - } else if !utils.IsUndefined(ex.Argus) { // Fallback to deprecated argus - argus := argus{} - diags = ex.Argus.As(ctx, &argus, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting extensions.argus object: %v", diags.Errors()) - } - argusEnabled := conversion.BoolValueToPointer(argus.Enabled) - argusInstanceId := conversion.StringValueToPointer(argus.ArgusInstanceId) - skeObservability = &ske.Observability{ - Enabled: argusEnabled, - InstanceId: argusInstanceId, - } - } - - var skeDNS *ske.DNS - if !(ex.DNS.IsNull() || ex.DNS.IsUnknown()) { - dns := dns{} - diags = ex.DNS.As(ctx, &dns, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting extensions.dns object: %v", diags.Errors()) - } - dnsEnabled := conversion.BoolValueToPointer(dns.Enabled) - - zones := []string{} - diags = dns.Zones.ElementsAs(ctx, &zones, true) - if diags.HasError() { - return nil, fmt.Errorf("converting extensions.dns.zones object: %v", diags.Errors()) - } - skeDNS = &ske.DNS{ - Enabled: dnsEnabled, - Zones: &zones, - } - } - - return &ske.Extension{ - Acl: skeAcl, - Observability: skeObservability, - Dns: skeDNS, - }, nil -} - -func parseMaintenanceWindowTime(t string) (time.Time, error) { - v, err := time.Parse("15:04:05-07:00", t) - if err != nil { - v, err = time.Parse("15:04:05Z", t) - } - return v, err -} - -func toMaintenancePayload(ctx context.Context, m *Model) (*ske.Maintenance, error) { - if m.Maintenance.IsNull() || m.Maintenance.IsUnknown() { - return nil, nil - } - - maintenance := maintenance{} - diags := m.Maintenance.As(ctx, &maintenance, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting maintenance object: %v", diags.Errors()) - } - - var timeWindowStart *time.Time - if !(maintenance.Start.IsNull() || maintenance.Start.IsUnknown()) { - tempTime, err := parseMaintenanceWindowTime(maintenance.Start.ValueString()) - if err != nil { - return nil, fmt.Errorf("converting maintenance object: %w", err) - } - timeWindowStart = sdkUtils.Ptr(tempTime) - } - - var timeWindowEnd *time.Time - if !(maintenance.End.IsNull() || maintenance.End.IsUnknown()) { - tempTime, err := parseMaintenanceWindowTime(maintenance.End.ValueString()) - if err != nil { - return nil, fmt.Errorf("converting maintenance object: %w", err) - } - timeWindowEnd = sdkUtils.Ptr(tempTime) - } - - return &ske.Maintenance{ - AutoUpdate: &ske.MaintenanceAutoUpdate{ - KubernetesVersion: conversion.BoolValueToPointer(maintenance.EnableKubernetesVersionUpdates), - MachineImageVersion: conversion.BoolValueToPointer(maintenance.EnableMachineImageVersionUpdates), - }, - TimeWindow: &ske.TimeWindow{ - Start: timeWindowStart, - End: timeWindowEnd, - }, - }, nil -} - -func toNetworkPayload(ctx context.Context, m *Model) (*ske.Network, error) { - if m.Network.IsNull() || m.Network.IsUnknown() { - return nil, nil - } - - network := network{} - diags := m.Network.As(ctx, &network, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return nil, fmt.Errorf("converting network object: %v", diags.Errors()) - } - - return &ske.Network{ - Id: conversion.StringValueToPointer(network.ID), - }, nil -} - -func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) error { - if cl == nil { - return fmt.Errorf("response input is nil") - } - if m == nil { - return fmt.Errorf("model input is nil") - } - - var name string - if m.Name.ValueString() != "" { - name = m.Name.ValueString() - } else if cl.Name != nil { - name = *cl.Name - } else { - return fmt.Errorf("name not present") - } - m.Name = types.StringValue(name) - - m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), region, name) - m.Region = types.StringValue(region) - - if cl.Kubernetes != nil { - m.KubernetesVersionUsed = types.StringPointerValue(cl.Kubernetes.Version) - } - - m.EgressAddressRanges = types.ListNull(types.StringType) - if cl.Status != nil { - var diags diag.Diagnostics - m.EgressAddressRanges, diags = types.ListValueFrom(ctx, types.StringType, cl.Status.EgressAddressRanges) - if diags.HasError() { - return fmt.Errorf("map egressAddressRanges: %w", core.DiagsToError(diags)) - } - } - - m.PodAddressRanges = types.ListNull(types.StringType) - if cl.Status != nil { - var diags diag.Diagnostics - m.PodAddressRanges, diags = types.ListValueFrom(ctx, types.StringType, cl.Status.PodAddressRanges) - if diags.HasError() { - return fmt.Errorf("map podAddressRanges: %w", core.DiagsToError(diags)) - } - } - - err := mapNodePools(ctx, cl, m) - if err != nil { - return fmt.Errorf("map node_pools: %w", err) - } - err = mapMaintenance(ctx, cl, m) - if err != nil { - return fmt.Errorf("map maintenance: %w", err) - } - err = mapNetwork(cl, m) - if err != nil { - return fmt.Errorf("map network: %w", err) - } - err = mapHibernations(cl, m) - if err != nil { - return fmt.Errorf("map hibernations: %w", err) - } - err = mapExtensions(ctx, cl, m) - if err != nil { - return fmt.Errorf("map extensions: %w", err) - } - return nil -} - -func mapNodePools(ctx context.Context, cl *ske.Cluster, model *Model) error { - modelNodePoolOSVersion := map[string]basetypes.StringValue{} - modelNodePoolOSVersionMin := map[string]basetypes.StringValue{} - - modelNodePools := []nodePool{} - if !model.NodePools.IsNull() && !model.NodePools.IsUnknown() { - diags := model.NodePools.ElementsAs(ctx, &modelNodePools, false) - if diags.HasError() { - return core.DiagsToError(diags) - } - } - - for i := range modelNodePools { - name := conversion.StringValueToPointer(modelNodePools[i].Name) - if name != nil { - modelNodePoolOSVersion[*name] = modelNodePools[i].OSVersion - modelNodePoolOSVersionMin[*name] = modelNodePools[i].OSVersionMin - } - } - - if cl.Nodepools == nil { - model.NodePools = types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}) - return nil - } - - nodePools := []attr.Value{} - for i, nodePoolResp := range *cl.Nodepools { - nodePool := map[string]attr.Value{ - "name": types.StringPointerValue(nodePoolResp.Name), - "machine_type": types.StringPointerValue(nodePoolResp.Machine.Type), - "os_name": types.StringNull(), - "os_version_min": modelNodePoolOSVersionMin[*nodePoolResp.Name], - "os_version": modelNodePoolOSVersion[*nodePoolResp.Name], - "minimum": types.Int64PointerValue(nodePoolResp.Minimum), - "maximum": types.Int64PointerValue(nodePoolResp.Maximum), - "max_surge": types.Int64PointerValue(nodePoolResp.MaxSurge), - "max_unavailable": types.Int64PointerValue(nodePoolResp.MaxUnavailable), - "volume_type": types.StringNull(), - "volume_size": types.Int64PointerValue(nodePoolResp.Volume.Size), - "labels": types.MapNull(types.StringType), - "cri": types.StringNull(), - "availability_zones": types.ListNull(types.StringType), - "allow_system_components": types.BoolPointerValue(nodePoolResp.AllowSystemComponents), - } - - if nodePoolResp.Machine != nil && nodePoolResp.Machine.Image != nil { - nodePool["os_name"] = types.StringPointerValue(nodePoolResp.Machine.Image.Name) - nodePool["os_version_used"] = types.StringPointerValue(nodePoolResp.Machine.Image.Version) - } - - if nodePoolResp.Volume != nil { - nodePool["volume_type"] = types.StringPointerValue(nodePoolResp.Volume.Type) - } - - if nodePoolResp.Cri != nil { - nodePool["cri"] = types.StringValue(string(nodePoolResp.Cri.GetName())) - } - - taintsInModel := false - if i < len(modelNodePools) && !modelNodePools[i].Taints.IsNull() && !modelNodePools[i].Taints.IsUnknown() { - taintsInModel = true - } - err := mapTaints(nodePoolResp.Taints, nodePool, taintsInModel) - if err != nil { - return fmt.Errorf("mapping index %d, field taints: %w", i, err) - } - - if nodePoolResp.Labels != nil { - elems := map[string]attr.Value{} - for k, v := range *nodePoolResp.Labels { - elems[k] = types.StringValue(v) - } - elemsTF, diags := types.MapValue(types.StringType, elems) - if diags.HasError() { - return fmt.Errorf("mapping index %d, field labels: %w", i, core.DiagsToError(diags)) - } - nodePool["labels"] = elemsTF - } - - if nodePoolResp.AvailabilityZones != nil { - elemsTF, diags := types.ListValueFrom(ctx, types.StringType, *nodePoolResp.AvailabilityZones) - if diags.HasError() { - return fmt.Errorf("mapping index %d, field availability_zones: %w", i, core.DiagsToError(diags)) - } - nodePool["availability_zones"] = elemsTF - } - - nodePoolTF, diags := basetypes.NewObjectValue(nodePoolTypes, nodePool) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - nodePools = append(nodePools, nodePoolTF) - } - nodePoolsTF, diags := basetypes.NewListValue(types.ObjectType{AttrTypes: nodePoolTypes}, nodePools) - if diags.HasError() { - return core.DiagsToError(diags) - } - model.NodePools = nodePoolsTF - return nil -} - -func mapTaints(t *[]ske.Taint, nodePool map[string]attr.Value, existInModel bool) error { - if t == nil || len(*t) == 0 { - if existInModel { - taintsTF, diags := types.ListValue(types.ObjectType{AttrTypes: taintTypes}, []attr.Value{}) - if diags.HasError() { - return fmt.Errorf("create empty taints list: %w", core.DiagsToError(diags)) - } - nodePool["taints"] = taintsTF - return nil - } - nodePool["taints"] = types.ListNull(types.ObjectType{AttrTypes: taintTypes}) - return nil - } - - taints := []attr.Value{} - - for i, taintResp := range *t { - taint := map[string]attr.Value{ - "effect": types.StringValue(string(taintResp.GetEffect())), - "key": types.StringPointerValue(taintResp.Key), - "value": types.StringPointerValue(taintResp.Value), - } - taintTF, diags := basetypes.NewObjectValue(taintTypes, taint) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - taints = append(taints, taintTF) - } - - taintsTF, diags := basetypes.NewListValue(types.ObjectType{AttrTypes: taintTypes}, taints) - if diags.HasError() { - return core.DiagsToError(diags) - } - - nodePool["taints"] = taintsTF - return nil -} - -func mapHibernations(cl *ske.Cluster, m *Model) error { - if cl.Hibernation == nil { - if !m.Hibernations.IsNull() { - emptyHibernations, diags := basetypes.NewListValue(basetypes.ObjectType{AttrTypes: hibernationTypes}, []attr.Value{}) - if diags.HasError() { - return fmt.Errorf("hibernations is an empty list, converting to terraform empty list: %w", core.DiagsToError(diags)) - } - m.Hibernations = emptyHibernations - return nil - } - m.Hibernations = basetypes.NewListNull(basetypes.ObjectType{AttrTypes: hibernationTypes}) - return nil - } - - if cl.Hibernation.Schedules == nil { - emptyHibernations, diags := basetypes.NewListValue(basetypes.ObjectType{AttrTypes: hibernationTypes}, []attr.Value{}) - if diags.HasError() { - return fmt.Errorf("hibernations is an empty list, converting to terraform empty list: %w", core.DiagsToError(diags)) - } - m.Hibernations = emptyHibernations - return nil - } - - hibernations := []attr.Value{} - for i, hibernationResp := range *cl.Hibernation.Schedules { - hibernation := map[string]attr.Value{ - "start": types.StringPointerValue(hibernationResp.Start), - "end": types.StringPointerValue(hibernationResp.End), - "timezone": types.StringPointerValue(hibernationResp.Timezone), - } - hibernationTF, diags := basetypes.NewObjectValue(hibernationTypes, hibernation) - if diags.HasError() { - return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) - } - hibernations = append(hibernations, hibernationTF) - } - - hibernationsTF, diags := basetypes.NewListValue(types.ObjectType{AttrTypes: hibernationTypes}, hibernations) - if diags.HasError() { - return core.DiagsToError(diags) - } - - m.Hibernations = hibernationsTF - return nil -} - -func mapMaintenance(ctx context.Context, cl *ske.Cluster, m *Model) error { - // Aligned with SKE team that a flattened data structure is fine, because no extensions are planned. - if cl.Maintenance == nil { - m.Maintenance = types.ObjectNull(maintenanceTypes) - return nil - } - ekvu := types.BoolNull() - if cl.Maintenance.AutoUpdate.KubernetesVersion != nil { - ekvu = types.BoolValue(*cl.Maintenance.AutoUpdate.KubernetesVersion) - } - emvu := types.BoolNull() - if cl.Maintenance.AutoUpdate.KubernetesVersion != nil { - emvu = types.BoolValue(*cl.Maintenance.AutoUpdate.MachineImageVersion) - } - startTime, endTime, err := getMaintenanceTimes(ctx, cl, m) - if err != nil { - return fmt.Errorf("getting maintenance times: %w", err) - } - maintenanceValues := map[string]attr.Value{ - "enable_kubernetes_version_updates": ekvu, - "enable_machine_image_version_updates": emvu, - "start": types.StringValue(startTime), - "end": types.StringValue(endTime), - } - maintenanceObject, diags := types.ObjectValue(maintenanceTypes, maintenanceValues) - if diags.HasError() { - return fmt.Errorf("create maintenance object: %w", core.DiagsToError(diags)) - } - m.Maintenance = maintenanceObject - return nil -} - -func mapNetwork(cl *ske.Cluster, m *Model) error { - if cl.Network == nil { - m.Network = types.ObjectNull(networkTypes) - return nil - } - - // If the network field is not provided, the SKE API returns an empty object. - // If we parse that object into the terraform model, it will produce an inconsistent result after apply error - - emptyNetwork := &ske.Network{} - if *cl.Network == *emptyNetwork && m.Network.IsNull() { - if m.Network.Attributes() == nil { - m.Network = types.ObjectNull(networkTypes) - } - return nil - } - - id := types.StringNull() - if cl.Network.Id != nil { - id = types.StringValue(*cl.Network.Id) - } - networkValues := map[string]attr.Value{ - "id": id, - } - networkObject, diags := types.ObjectValue(networkTypes, networkValues) - if diags.HasError() { - return fmt.Errorf("create network object: %w", core.DiagsToError(diags)) - } - m.Network = networkObject - return nil -} - -func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startTime, endTime string, err error) { - startTimeAPI := *cl.Maintenance.TimeWindow.Start - endTimeAPI := *cl.Maintenance.TimeWindow.End - - if m.Maintenance.IsNull() || m.Maintenance.IsUnknown() { - return startTimeAPI.Format("15:04:05Z07:00"), endTimeAPI.Format("15:04:05Z07:00"), nil - } - - maintenance := &maintenance{} - diags := m.Maintenance.As(ctx, maintenance, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return "", "", fmt.Errorf("converting maintenance object %w", core.DiagsToError(diags.Errors())) - } - - startTime = startTimeAPI.Format("15:04:05Z07:00") - if !(maintenance.Start.IsNull() || maintenance.Start.IsUnknown()) { - startTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.Start.ValueString()) - if err != nil { - return "", "", fmt.Errorf("parsing start time '%s' from TF config as RFC time: %w", maintenance.Start.ValueString(), err) - } - // If the start times from the API and the TF model just differ in format, we keep the current TF model value - if startTimeAPI.Format("15:04:05Z07:00") == startTimeTF.Format("15:04:05Z07:00") { - startTime = maintenance.Start.ValueString() - } - } - - endTime = endTimeAPI.Format("15:04:05Z07:00") - if !(maintenance.End.IsNull() || maintenance.End.IsUnknown()) { - endTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.End.ValueString()) - if err != nil { - return "", "", fmt.Errorf("parsing end time '%s' from TF config as RFC time: %w", maintenance.End.ValueString(), err) - } - // If the end times from the API and the TF model just differ in format, we keep the current TF model value - if endTimeAPI.Format("15:04:05Z07:00") == endTimeTF.Format("15:04:05Z07:00") { - endTime = maintenance.End.ValueString() - } - } - - return startTime, endTime, nil -} - -func checkDisabledExtensions(ctx context.Context, ex *extensions) (aclDisabled, observabilityDisabled, dnsDisabled bool, err error) { - var diags diag.Diagnostics - acl := acl{} - if ex.ACL.IsNull() { - acl.Enabled = types.BoolValue(false) - } else { - diags = ex.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return false, false, false, fmt.Errorf("converting extensions.acl object: %v", diags.Errors()) - } - } - - observability := observability{} - if ex.Argus.IsNull() && ex.Observability.IsNull() { - observability.Enabled = types.BoolValue(false) - } else if !ex.Argus.IsNull() { - argus := argus{} - diags = ex.Argus.As(ctx, &argus, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return false, false, false, fmt.Errorf("converting extensions.argus object: %v", diags.Errors()) - } - observability.Enabled = argus.Enabled - observability.InstanceId = argus.ArgusInstanceId - } else { - diags = ex.Observability.As(ctx, &observability, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return false, false, false, fmt.Errorf("converting extensions.observability object: %v", diags.Errors()) - } - } - - dns := dns{} - if ex.DNS.IsNull() { - dns.Enabled = types.BoolValue(false) - } else { - diags = ex.DNS.As(ctx, &dns, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return false, false, false, fmt.Errorf("converting extensions.dns object: %v", diags.Errors()) - } - } - - return !acl.Enabled.ValueBool(), !observability.Enabled.ValueBool(), !dns.Enabled.ValueBool(), nil -} - -func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { - if cl.Extensions == nil { - m.Extensions = types.ObjectNull(extensionsTypes) - return nil - } - - var diags diag.Diagnostics - ex := extensions{} - if !m.Extensions.IsNull() { - diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return fmt.Errorf("converting extensions object: %v", diags.Errors()) - } - } - - // If the user provides the extensions block with the enabled flags as false - // the SKE API will return an empty extensions block, which throws an inconsistent - // result after apply error. To prevent this error, if both flags are false, - // we set the fields provided by the user in the terraform model - - // If the extensions field is not provided, the SKE API returns an empty object. - // If we parse that object into the terraform model, it will produce an inconsistent result after apply - // error - - aclDisabled, observabilityDisabled, dnsDisabled, err := checkDisabledExtensions(ctx, &ex) - if err != nil { - return fmt.Errorf("checking if extensions are disabled: %w", err) - } - disabledExtensions := false - if aclDisabled && observabilityDisabled && dnsDisabled { - disabledExtensions = true - } - - emptyExtensions := &ske.Extension{} - if *cl.Extensions == *emptyExtensions && (disabledExtensions || m.Extensions.IsNull()) { - if m.Extensions.Attributes() == nil { - m.Extensions = types.ObjectNull(extensionsTypes) - } - return nil - } - - aclExtension := types.ObjectNull(aclTypes) - if cl.Extensions.Acl != nil { - enabled := types.BoolNull() - if cl.Extensions.Acl.Enabled != nil { - enabled = types.BoolValue(*cl.Extensions.Acl.Enabled) - } - - cidrsList, diags := types.ListValueFrom(ctx, types.StringType, cl.Extensions.Acl.AllowedCidrs) - if diags.HasError() { - return fmt.Errorf("creating allowed_cidrs list: %w", core.DiagsToError(diags)) - } - - aclValues := map[string]attr.Value{ - "enabled": enabled, - "allowed_cidrs": cidrsList, - } - - aclExtension, diags = types.ObjectValue(aclTypes, aclValues) - if diags.HasError() { - return fmt.Errorf("creating acl: %w", core.DiagsToError(diags)) - } - } else if aclDisabled && !ex.ACL.IsNull() { - aclExtension = ex.ACL - } - - // Deprecated: argus won't be received from backend. Use observabilty instead. - argusExtension := types.ObjectNull(argusTypes) - observabilityExtension := types.ObjectNull(observabilityTypes) - if cl.Extensions.Observability != nil { - enabled := types.BoolNull() - if cl.Extensions.Observability.Enabled != nil { - enabled = types.BoolValue(*cl.Extensions.Observability.Enabled) - } - - observabilityInstanceId := types.StringNull() - if cl.Extensions.Observability.InstanceId != nil { - observabilityInstanceId = types.StringValue(*cl.Extensions.Observability.InstanceId) - } - - observabilityExtensionValues := map[string]attr.Value{ - "enabled": enabled, - "instance_id": observabilityInstanceId, - } - - argusExtensionValues := map[string]attr.Value{ - "enabled": enabled, - "argus_instance_id": observabilityInstanceId, - } - - observabilityExtension, diags = types.ObjectValue(observabilityTypes, observabilityExtensionValues) - if diags.HasError() { - return fmt.Errorf("creating observability extension: %w", core.DiagsToError(diags)) - } - argusExtension, diags = types.ObjectValue(argusTypes, argusExtensionValues) - if diags.HasError() { - return fmt.Errorf("creating argus extension: %w", core.DiagsToError(diags)) - } - } else if observabilityDisabled && !ex.Observability.IsNull() { - observabilityExtension = ex.Observability - - // set deprecated argus extension - observability := observability{} - diags = ex.Observability.As(ctx, &observability, basetypes.ObjectAsOptions{}) - if diags.HasError() { - return fmt.Errorf("converting extensions.observability object: %v", diags.Errors()) - } - argusExtensionValues := map[string]attr.Value{ - "enabled": observability.Enabled, - "argus_instance_id": observability.InstanceId, - } - argusExtension, diags = types.ObjectValue(argusTypes, argusExtensionValues) - if diags.HasError() { - return fmt.Errorf("creating argus extension: %w", core.DiagsToError(diags)) - } - } - - dnsExtension := types.ObjectNull(dnsTypes) - if cl.Extensions.Dns != nil { - enabled := types.BoolNull() - if cl.Extensions.Dns.Enabled != nil { - enabled = types.BoolValue(*cl.Extensions.Dns.Enabled) - } - - zonesList, diags := types.ListValueFrom(ctx, types.StringType, cl.Extensions.Dns.Zones) - if diags.HasError() { - return fmt.Errorf("creating zones list: %w", core.DiagsToError(diags)) - } - - dnsValues := map[string]attr.Value{ - "enabled": enabled, - "zones": zonesList, - } - - dnsExtension, diags = types.ObjectValue(dnsTypes, dnsValues) - if diags.HasError() { - return fmt.Errorf("creating dns: %w", core.DiagsToError(diags)) - } - } else if dnsDisabled && !ex.DNS.IsNull() { - dnsExtension = ex.DNS - } - - // Deprecation: Argus was renamed to observability. Depending on which attribute was used in the terraform config the - // according one has to be set here. - var extensionsValues map[string]attr.Value - if utils.IsUndefined(ex.Argus) { - extensionsValues = map[string]attr.Value{ - "acl": aclExtension, - "argus": types.ObjectNull(argusTypes), - "observability": observabilityExtension, - "dns": dnsExtension, - } - } else { - extensionsValues = map[string]attr.Value{ - "acl": aclExtension, - "argus": argusExtension, - "observability": types.ObjectNull(observabilityTypes), - "dns": dnsExtension, - } - } - - extensions, diags := types.ObjectValue(extensionsTypes, extensionsValues) - if diags.HasError() { - return fmt.Errorf("creating extensions: %w", core.DiagsToError(diags)) - } - m.Extensions = extensions - return nil -} - -func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion, currentKubernetesVersion *string, diags *diag.Diagnostics) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) { - providedVersionMin := m.KubernetesVersionMin.ValueStringPointer() - versionUsed, hasDeprecatedVersion, err := latestMatchingKubernetesVersion(availableVersions, providedVersionMin, currentKubernetesVersion, diags) - if err != nil { - return nil, false, fmt.Errorf("getting latest matching kubernetes version: %w", err) - } - - k := &ske.Kubernetes{ - Version: versionUsed, - } - return k, hasDeprecatedVersion, nil -} - -func latestMatchingKubernetesVersion(availableVersions []ske.KubernetesVersion, kubernetesVersionMin, currentKubernetesVersion *string, diags *diag.Diagnostics) (version *string, deprecated bool, err error) { - if availableVersions == nil { - return nil, false, fmt.Errorf("nil available kubernetes versions") - } - - if kubernetesVersionMin == nil { - if currentKubernetesVersion == nil { - latestVersion, err := getLatestSupportedKubernetesVersion(availableVersions) - if err != nil { - return nil, false, fmt.Errorf("get latest supported kubernetes version: %w", err) - } - return latestVersion, false, nil - } - kubernetesVersionMin = currentKubernetesVersion - } else if currentKubernetesVersion != nil { - // For an already existing cluster, if kubernetes_version_min is set to a lower version than what is being used in the cluster - // return the currently used version - kubernetesVersionUsed := *currentKubernetesVersion - kubernetesVersionMinString := *kubernetesVersionMin - - minVersionPrefixed := "v" + kubernetesVersionMinString - usedVersionPrefixed := "v" + kubernetesVersionUsed - - if semver.Compare(minVersionPrefixed, usedVersionPrefixed) == -1 { - kubernetesVersionMin = currentKubernetesVersion - } - } - - versionRegex := regexp.MustCompile(validate.FullVersionRegex) - fullVersion := versionRegex.MatchString(*kubernetesVersionMin) - - providedVersionPrefixed := "v" + *kubernetesVersionMin - if !semver.IsValid(providedVersionPrefixed) { - return nil, false, fmt.Errorf("provided version is invalid") - } - - var ( - selectedVersion *ske.KubernetesVersion - availableVersionsArray []string - ) - if fullVersion { - availableVersionsArray, selectedVersion = selectFullVersion(availableVersions, providedVersionPrefixed) - } else { - availableVersionsArray, selectedVersion = selectMatchingVersion(availableVersions, providedVersionPrefixed) - } - - deprecated = isDeprecated(selectedVersion) - - if isPreview(selectedVersion) { - diags.AddWarning("preview version selected", fmt.Sprintf("only the preview version %q matched the selection criteria", *selectedVersion.Version)) - } - - // Throwing error if we could not match the version with the available versions - if selectedVersion == nil { - return nil, false, fmt.Errorf("provided version is not one of the available kubernetes versions, available versions are: %s", strings.Join(availableVersionsArray, ",")) - } - - return selectedVersion.Version, deprecated, nil -} - -func selectFullVersion(availableVersions []ske.KubernetesVersion, kubernetesVersionMin string) (availableVersionsArray []string, selectedVersion *ske.KubernetesVersion) { - for _, versionCandidate := range availableVersions { - if versionCandidate.State == nil || versionCandidate.Version == nil { - continue - } - availableVersionsArray = append(availableVersionsArray, *versionCandidate.Version) - vPrefixed := "v" + *versionCandidate.Version - - // [MAJOR].[MINOR].[PATCH] version provided, match available version - if semver.Compare(vPrefixed, kubernetesVersionMin) == 0 { - selectedVersion = &versionCandidate - break - } - } - return availableVersionsArray, selectedVersion -} - -func selectMatchingVersion(availableVersions []ske.KubernetesVersion, kubernetesVersionMin string) (availableVersionsArray []string, selectedVersion *ske.KubernetesVersion) { - sortK8sVersions(availableVersions) - for _, candidateVersion := range availableVersions { - if candidateVersion.State == nil || candidateVersion.Version == nil { - continue - } - availableVersionsArray = append(availableVersionsArray, *candidateVersion.Version) - vPreffixed := "v" + *candidateVersion.Version - - // [MAJOR].[MINOR] version provided, get the latest non-preview patch version - if semver.MajorMinor(vPreffixed) == semver.MajorMinor(kubernetesVersionMin) && - (semver.Compare(vPreffixed, kubernetesVersionMin) >= 0) && - (candidateVersion.State != nil) { - // take the current version as a candidate, if we have no other version inspected before - // OR the previously found version was a preview version - if selectedVersion == nil || (isSupported(&candidateVersion) && isPreview(selectedVersion)) { - selectedVersion = &candidateVersion - } - // all other cases are ignored - } - } - return availableVersionsArray, selectedVersion -} - -func isDeprecated(v *ske.KubernetesVersion) bool { - if v == nil { - return false - } - - if v.State == nil { - return false - } - - return *v.State == VersionStateDeprecated -} - -func isPreview(v *ske.KubernetesVersion) bool { - if v == nil { - return false - } - - if v.State == nil { - return false - } - - return *v.State == VersionStatePreview -} - -func isSupported(v *ske.KubernetesVersion) bool { - if v == nil { - return false - } - - if v.State == nil { - return false - } - - return *v.State == VersionStateSupported -} - -func getLatestSupportedKubernetesVersion(versions []ske.KubernetesVersion) (*string, error) { - foundKubernetesVersion := false - var latestVersion *string - for i := range versions { - version := versions[i] - if *version.State != VersionStateSupported { - continue - } - if latestVersion != nil { - oldSemVer := fmt.Sprintf("v%s", *latestVersion) - newSemVer := fmt.Sprintf("v%s", *version.Version) - if semver.Compare(newSemVer, oldSemVer) != 1 { - continue - } - } - - foundKubernetesVersion = true - latestVersion = version.Version - } - if !foundKubernetesVersion { - return nil, fmt.Errorf("no supported Kubernetes version found") - } - return latestVersion, nil -} - -func hasKubernetesMinChanged(ctx context.Context, request planmodifier.StringRequest, response *utils.UseStateForUnknownFuncResponse) { // nolint:gocritic // function signature required by Terraform - dependencyPath := path.Root("kubernetes_version_min") - - var minVersionPlan types.String - diags := request.Plan.GetAttribute(ctx, dependencyPath, &minVersionPlan) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - var minVersionState types.String - diags = request.State.GetAttribute(ctx, dependencyPath, &minVersionState) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - if minVersionState == minVersionPlan { - response.UseStateForUnknown = true - return - } -} - -func hasOsVersionMinChanged(ctx context.Context, request planmodifier.StringRequest, response *utils.UseStateForUnknownFuncResponse) { // nolint:gocritic // function signature required by Terraform - dependencyPath := request.Path.ParentPath().AtName("os_version_min") - - var minVersionPlan types.String - diags := request.Plan.GetAttribute(ctx, dependencyPath, &minVersionPlan) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - var minVersionState types.String - diags = request.State.GetAttribute(ctx, dependencyPath, &minVersionState) - response.Diagnostics.Append(diags...) - if response.Diagnostics.HasError() { - return - } - - if minVersionState == minVersionPlan { - response.UseStateForUnknown = true - return - } -} - -func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var state Model - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := state.ProjectId.ValueString() - name := state.Name.ValueString() - region := r.providerData.GetRegionWithOverride(state.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "region", region) - - clResp, err := r.skeClient.GetCluster(ctx, projectId, region, name).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - err = mapFields(ctx, clResp, &state, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - diags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SKE cluster read") -} - -func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - clName := model.Name.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", clName) - ctx = tflog.SetField(ctx, "region", region) - - availableKubernetesVersions, availableMachines, err := r.loadAvailableVersions(ctx, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating cluster", fmt.Sprintf("Loading available Kubernetes and machine image versions: %v", err)) - return - } - - currentKubernetesVersion, currentMachineImages := getCurrentVersions(ctx, r.skeClient, &model) - - r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableKubernetesVersions, availableMachines, currentKubernetesVersion, currentMachineImages) - if resp.Diagnostics.HasError() { - return - } - - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SKE cluster updated") -} - -func (r *clusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - resp.Diagnostics.Append(req.State.Get(ctx, &model)...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - name := model.Name.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "name", name) - ctx = tflog.SetField(ctx, "region", region) - - c := r.skeClient - _, err := c.DeleteCluster(ctx, projectId, region, name).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting cluster", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = skeWait.DeleteClusterWaitHandler(ctx, r.skeClient, projectId, region, name).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting cluster", fmt.Sprintf("Cluster deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "SKE cluster deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name -func (r *clusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing cluster", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[name] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) - tflog.Info(ctx, "SKE cluster state imported") -} diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go deleted file mode 100644 index e29c15cc..00000000 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ /dev/null @@ -1,2639 +0,0 @@ -package ske - -import ( - "context" - "fmt" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" -) - -type skeClientMocked struct { - returnError bool - getClusterResp *ske.Cluster -} - -const testRegion = "region" - -func (c *skeClientMocked) GetClusterExecute(_ context.Context, _, _, _ string) (*ske.Cluster, error) { - if c.returnError { - return nil, fmt.Errorf("get cluster failed") - } - - return c.getClusterResp, nil -} - -func TestMapFields(t *testing.T) { - cs := ske.ClusterStatusState("OK") - tests := []struct { - description string - stateExtensions types.Object - stateNodePools types.List - input *ske.Cluster - region string - expected Model - isValid bool - }{ - { - "default_values", - types.ObjectNull(extensionsTypes), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{ - Name: utils.Ptr("name"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - Maintenance: types.ObjectNull(maintenanceTypes), - Network: types.ObjectNull(networkTypes), - Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), - Extensions: types.ObjectNull(extensionsTypes), - EgressAddressRanges: types.ListNull(types.StringType), - PodAddressRanges: types.ListNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - types.ObjectNull(extensionsTypes), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{ - Extensions: &ske.Extension{ - Acl: &ske.ACL{ - AllowedCidrs: &[]string{"cidr1"}, - Enabled: utils.Ptr(true), - }, - Observability: &ske.Observability{ - InstanceId: utils.Ptr("aid"), - Enabled: utils.Ptr(true), - }, - Dns: &ske.DNS{ - Zones: &[]string{"foo.onstackit.cloud"}, - Enabled: utils.Ptr(true), - }, - }, - Hibernation: &ske.Hibernation{ - Schedules: &[]ske.HibernationSchedule{ - { - End: utils.Ptr("2"), - Start: utils.Ptr("1"), - Timezone: utils.Ptr("CET"), - }, - }, - }, - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("1.2.3"), - }, - Maintenance: &ske.Maintenance{ - AutoUpdate: &ske.MaintenanceAutoUpdate{ - KubernetesVersion: utils.Ptr(true), - MachineImageVersion: utils.Ptr(true), - }, - TimeWindow: &ske.TimeWindow{ - Start: utils.Ptr(time.Date(0, 1, 2, 3, 4, 5, 6, time.FixedZone("UTC+6:00", 6*60*60))), - End: utils.Ptr(time.Date(10, 11, 12, 13, 14, 15, 0, time.UTC)), - }, - }, - Network: &ske.Network{ - Id: utils.Ptr("nid"), - }, - Name: utils.Ptr("name"), - Nodepools: &[]ske.Nodepool{ - { - AllowSystemComponents: utils.Ptr(true), - AvailabilityZones: &[]string{"z1", "z2"}, - Cri: &ske.CRI{ - Name: ske.CRINAME_DOCKER.Ptr(), - }, - Labels: &map[string]string{"k": "v"}, - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: utils.Ptr("os"), - Version: utils.Ptr("os-ver"), - }, - Type: utils.Ptr("B"), - }, - MaxSurge: utils.Ptr(int64(3)), - MaxUnavailable: nil, - Maximum: utils.Ptr(int64(5)), - Minimum: utils.Ptr(int64(1)), - Name: utils.Ptr("node"), - Taints: &[]ske.Taint{ - { - Effect: ske.TAINTEFFECT_NO_EXECUTE.Ptr(), - Key: utils.Ptr("key"), - Value: utils.Ptr("value"), - }, - }, - Volume: &ske.Volume{ - Size: utils.Ptr(int64(3)), - Type: utils.Ptr("type"), - }, - }, - }, - Status: &ske.ClusterStatus{ - Aggregated: &cs, - Error: nil, - Hibernated: nil, - EgressAddressRanges: &[]string{"0.0.0.0/32", "1.1.1.1/32"}, - PodAddressRanges: &[]string{"0.0.0.0/32", "1.1.1.1/32"}, - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - KubernetesVersionUsed: types.StringValue("1.2.3"), - EgressAddressRanges: types.ListValueMust( - types.StringType, - []attr.Value{ - types.StringValue("0.0.0.0/32"), - types.StringValue("1.1.1.1/32"), - }, - ), - PodAddressRanges: types.ListValueMust( - types.StringType, - []attr.Value{ - types.StringValue("0.0.0.0/32"), - types.StringValue("1.1.1.1/32"), - }, - ), - NodePools: types.ListValueMust( - types.ObjectType{AttrTypes: nodePoolTypes}, - []attr.Value{ - types.ObjectValueMust( - nodePoolTypes, - map[string]attr.Value{ - "name": types.StringValue("node"), - "machine_type": types.StringValue("B"), - "os_name": types.StringValue("os"), - "os_version": types.StringNull(), - "os_version_min": types.StringNull(), - "os_version_used": types.StringValue("os-ver"), - "minimum": types.Int64Value(1), - "maximum": types.Int64Value(5), - "max_surge": types.Int64Value(3), - "max_unavailable": types.Int64Null(), - "volume_type": types.StringValue("type"), - "volume_size": types.Int64Value(3), - "labels": types.MapValueMust( - types.StringType, - map[string]attr.Value{ - "k": types.StringValue("v"), - }, - ), - "taints": types.ListValueMust( - types.ObjectType{AttrTypes: taintTypes}, - []attr.Value{ - types.ObjectValueMust( - taintTypes, - map[string]attr.Value{ - "effect": types.StringValue(string(ske.TAINTEFFECT_NO_EXECUTE)), - "key": types.StringValue("key"), - "value": types.StringValue("value"), - }, - ), - }, - ), - "cri": types.StringValue(string(ske.CRINAME_DOCKER)), - "availability_zones": types.ListValueMust( - types.StringType, - []attr.Value{ - types.StringValue("z1"), - types.StringValue("z2"), - }, - ), - "allow_system_components": types.BoolValue(true), - }, - ), - }, - ), - Maintenance: types.ObjectValueMust(maintenanceTypes, map[string]attr.Value{ - "enable_kubernetes_version_updates": types.BoolValue(true), - "enable_machine_image_version_updates": types.BoolValue(true), - "start": types.StringValue("03:04:05+06:00"), - "end": types.StringValue("13:14:15Z"), - }), - Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "id": types.StringValue("nid"), - }), - Hibernations: types.ListValueMust( - types.ObjectType{AttrTypes: hibernationTypes}, - []attr.Value{ - types.ObjectValueMust( - hibernationTypes, - map[string]attr.Value{ - "start": types.StringValue("1"), - "end": types.StringValue("2"), - "timezone": types.StringValue("CET"), - }, - ), - }, - ), - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "allowed_cidrs": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("cidr1"), - }), - }), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "instance_id": types.StringValue("aid"), - }), - "dns": types.ObjectValueMust(dnsTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "zones": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("foo.onstackit.cloud"), - }), - }), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "empty_network", - types.ObjectNull(extensionsTypes), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{ - Name: utils.Ptr("name"), - Network: &ske.Network{}, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - Maintenance: types.ObjectNull(maintenanceTypes), - Network: types.ObjectNull(networkTypes), - Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), - Extensions: types.ObjectNull(extensionsTypes), - EgressAddressRanges: types.ListNull(types.StringType), - PodAddressRanges: types.ListNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "extensions_mixed_values", - types.ObjectNull(extensionsTypes), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{ - Extensions: &ske.Extension{ - Acl: &ske.ACL{ - AllowedCidrs: nil, - Enabled: utils.Ptr(true), - }, - Observability: &ske.Observability{ - InstanceId: nil, - Enabled: utils.Ptr(true), - }, - Dns: &ske.DNS{ - Zones: nil, - Enabled: utils.Ptr(true), - }, - }, - Name: utils.Ptr("name"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - Maintenance: types.ObjectNull(maintenanceTypes), - Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), - EgressAddressRanges: types.ListNull(types.StringType), - PodAddressRanges: types.ListNull(types.StringType), - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "allowed_cidrs": types.ListNull(types.StringType), - }), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "instance_id": types.StringNull(), - }), - "dns": types.ObjectValueMust(dnsTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "zones": types.ListNull(types.StringType), - }), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "extensions_disabled", - types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "allowed_cidrs": types.ListNull(types.StringType), - }), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "instance_id": types.StringNull(), - }), - "dns": types.ObjectValueMust(dnsTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "zones": types.ListNull(types.StringType), - }), - }), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{ - Extensions: &ske.Extension{}, - Name: utils.Ptr("name"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - Maintenance: types.ObjectNull(maintenanceTypes), - Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), - EgressAddressRanges: types.ListNull(types.StringType), - PodAddressRanges: types.ListNull(types.StringType), - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "allowed_cidrs": types.ListNull(types.StringType), - }), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "instance_id": types.StringNull(), - }), - "dns": types.ObjectValueMust(dnsTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "zones": types.ListNull(types.StringType), - }), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "extensions_only_observability_disabled", - types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "allowed_cidrs": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("cidr1"), - }), - }), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "instance_id": types.StringValue("id"), - }), - "dns": types.ObjectValueMust(dnsTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "zones": types.ListNull(types.StringType), - }), - }), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{ - Extensions: &ske.Extension{ - Acl: &ske.ACL{ - AllowedCidrs: &[]string{"cidr1"}, - Enabled: utils.Ptr(true), - }, - Dns: &ske.DNS{ - Zones: nil, - Enabled: utils.Ptr(true), - }, - }, - Name: utils.Ptr("name"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - Maintenance: types.ObjectNull(maintenanceTypes), - Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), - EgressAddressRanges: types.ListNull(types.StringType), - PodAddressRanges: types.ListNull(types.StringType), - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "allowed_cidrs": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("cidr1"), - }), - }), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(false), - "instance_id": types.StringValue("id"), - }), - "dns": types.ObjectValueMust(dnsTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "zones": types.ListNull(types.StringType), - }), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "extensions_not_set", - types.ObjectNull(extensionsTypes), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{ - Extensions: &ske.Extension{}, - Name: utils.Ptr("name"), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - Maintenance: types.ObjectNull(maintenanceTypes), - Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), - Extensions: types.ObjectNull(extensionsTypes), - EgressAddressRanges: types.ListNull(types.StringType), - PodAddressRanges: types.ListNull(types.StringType), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_taints_when_empty_list_on_state", - types.ObjectNull(extensionsTypes), - types.ListValueMust( - types.ObjectType{AttrTypes: nodePoolTypes}, - []attr.Value{ - types.ObjectValueMust( - nodePoolTypes, - map[string]attr.Value{ - "name": types.StringValue("node"), - "machine_type": types.StringValue("B"), - "os_name": types.StringValue("os"), - "os_version": types.StringNull(), - "os_version_min": types.StringNull(), - "os_version_used": types.StringValue("os-ver"), - "minimum": types.Int64Value(1), - "maximum": types.Int64Value(5), - "max_surge": types.Int64Value(3), - "max_unavailable": types.Int64Null(), - "volume_type": types.StringValue("type"), - "volume_size": types.Int64Value(3), - "labels": types.MapValueMust( - types.StringType, - map[string]attr.Value{ - "k": types.StringValue("v"), - }, - ), - "taints": types.ListValueMust(types.ObjectType{AttrTypes: taintTypes}, []attr.Value{}), - "cri": types.StringValue(string(ske.CRINAME_DOCKER)), - "availability_zones": types.ListValueMust( - types.StringType, - []attr.Value{ - types.StringValue("z1"), - types.StringValue("z2"), - }, - ), - "allow_system_components": types.BoolValue(true), - }, - ), - }, - ), - &ske.Cluster{ - Extensions: &ske.Extension{ - Acl: &ske.ACL{ - AllowedCidrs: &[]string{"cidr1"}, - Enabled: utils.Ptr(true), - }, - Observability: &ske.Observability{ - InstanceId: utils.Ptr("aid"), - Enabled: utils.Ptr(true), - }, - Dns: &ske.DNS{ - Zones: &[]string{"zone1"}, - Enabled: utils.Ptr(true), - }, - }, - Hibernation: &ske.Hibernation{ - Schedules: &[]ske.HibernationSchedule{ - { - End: utils.Ptr("2"), - Start: utils.Ptr("1"), - Timezone: utils.Ptr("CET"), - }, - }, - }, - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("1.2.3"), - }, - Maintenance: &ske.Maintenance{ - AutoUpdate: &ske.MaintenanceAutoUpdate{ - KubernetesVersion: utils.Ptr(true), - MachineImageVersion: utils.Ptr(true), - }, - TimeWindow: &ske.TimeWindow{ - Start: utils.Ptr(time.Date(0, 1, 2, 3, 4, 5, 6, time.FixedZone("UTC+6:00", 6*60*60))), - End: utils.Ptr(time.Date(10, 11, 12, 13, 14, 15, 0, time.UTC)), - }, - }, - Network: &ske.Network{ - Id: utils.Ptr("nid"), - }, - Name: utils.Ptr("name"), - Nodepools: &[]ske.Nodepool{ - { - AvailabilityZones: &[]string{"z1", "z2"}, - Cri: &ske.CRI{ - Name: ske.CRINAME_DOCKER.Ptr(), - }, - Labels: &map[string]string{"k": "v"}, - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: utils.Ptr("os"), - Version: utils.Ptr("os-ver"), - }, - Type: utils.Ptr("B"), - }, - MaxSurge: utils.Ptr(int64(3)), - MaxUnavailable: nil, - Maximum: utils.Ptr(int64(5)), - Minimum: utils.Ptr(int64(1)), - Name: utils.Ptr("node"), - Taints: nil, - Volume: &ske.Volume{ - Size: utils.Ptr(int64(3)), - Type: utils.Ptr("type"), - }, - }, - }, - Status: &ske.ClusterStatus{ - Aggregated: &cs, - Error: nil, - Hibernated: nil, - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,name"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - KubernetesVersionUsed: types.StringValue("1.2.3"), - EgressAddressRanges: types.ListNull(types.StringType), - PodAddressRanges: types.ListNull(types.StringType), - NodePools: types.ListValueMust( - types.ObjectType{AttrTypes: nodePoolTypes}, - []attr.Value{ - types.ObjectValueMust( - nodePoolTypes, - map[string]attr.Value{ - "name": types.StringValue("node"), - "machine_type": types.StringValue("B"), - "os_name": types.StringValue("os"), - "os_version": types.StringNull(), - "os_version_min": types.StringNull(), - "os_version_used": types.StringValue("os-ver"), - "minimum": types.Int64Value(1), - "maximum": types.Int64Value(5), - "max_surge": types.Int64Value(3), - "max_unavailable": types.Int64Null(), - "volume_type": types.StringValue("type"), - "volume_size": types.Int64Value(3), - "labels": types.MapValueMust( - types.StringType, - map[string]attr.Value{ - "k": types.StringValue("v"), - }, - ), - "taints": types.ListValueMust(types.ObjectType{AttrTypes: taintTypes}, []attr.Value{}), - "cri": types.StringValue(string(ske.CRINAME_DOCKER)), - "availability_zones": types.ListValueMust( - types.StringType, - []attr.Value{ - types.StringValue("z1"), - types.StringValue("z2"), - }, - ), - "allow_system_components": types.BoolNull(), - }, - ), - }, - ), - Maintenance: types.ObjectValueMust(maintenanceTypes, map[string]attr.Value{ - "enable_kubernetes_version_updates": types.BoolValue(true), - "enable_machine_image_version_updates": types.BoolValue(true), - "start": types.StringValue("03:04:05+06:00"), - "end": types.StringValue("13:14:15Z"), - }), - Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "id": types.StringValue("nid"), - }), - Hibernations: types.ListValueMust( - types.ObjectType{AttrTypes: hibernationTypes}, - []attr.Value{ - types.ObjectValueMust( - hibernationTypes, - map[string]attr.Value{ - "start": types.StringValue("1"), - "end": types.StringValue("2"), - "timezone": types.StringValue("CET"), - }, - ), - }, - ), - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "allowed_cidrs": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("cidr1"), - }), - }), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "instance_id": types.StringValue("aid"), - }), - "dns": types.ObjectValueMust(dnsTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "zones": types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("zone1"), - }), - }), - }), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - types.ObjectNull(extensionsTypes), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - nil, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - types.ObjectNull(extensionsTypes), - types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), - &ske.Cluster{}, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - Extensions: tt.stateExtensions, - NodePools: tt.stateNodePools, - } - err := mapFields(context.Background(), tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestLatestMatchingKubernetesVersion(t *testing.T) { - tests := []struct { - description string - availableVersions []ske.KubernetesVersion - kubernetesVersionMin *string - currentKubernetesVersion *string - expectedVersionUsed *string - expectedHasDeprecatedVersion bool - expectedWarning bool - isValid bool - }{ - { - "available_version", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20.1"), - nil, - utils.Ptr("1.20.1"), - false, - false, - true, - }, - { - "available_version_zero_patch", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20.0"), - nil, - utils.Ptr("1.20.0"), - false, - false, - true, - }, - { - "available_version_with_no_provided_patch", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20"), - nil, - utils.Ptr("1.20.2"), - false, - false, - true, - }, - { - "available_version_with_higher_preview_patch_not_selected", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStatePreview), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20"), - nil, - utils.Ptr("1.20.1"), - false, - false, - true, - }, - { - "available_version_no_provided_patch_2", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20"), - nil, - utils.Ptr("1.20.0"), - false, - false, - true, - }, - { - "deprecated_version", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateDeprecated), - }, - }, - utils.Ptr("1.19"), - nil, - utils.Ptr("1.19.0"), - true, - false, - true, - }, - { - "preview_version", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStatePreview), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20.0"), - nil, - utils.Ptr("1.20.0"), - false, - true, - true, - }, - { - "nil_provided_version_get_latest", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - nil, - nil, - utils.Ptr("1.20.0"), - false, - false, - true, - }, - { - "nil_provided_version_use_current", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - nil, - utils.Ptr("1.19.0"), - utils.Ptr("1.19.0"), - false, - false, - true, - }, - { - "update_lower_min_provided", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.19"), - utils.Ptr("1.20.0"), - utils.Ptr("1.20.0"), - false, - false, - true, - }, - { - "update_lower_min_provided_deprecated_version", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.21.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateDeprecated), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateDeprecated), - }, - }, - utils.Ptr("1.19"), - utils.Ptr("1.20.0"), - utils.Ptr("1.20.0"), - true, - false, - true, - }, - { - "update_matching_min_provided", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20"), - utils.Ptr("1.20.0"), - utils.Ptr("1.20.0"), - false, - false, - true, - }, - { - "update_higher_min_provided", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.20"), - utils.Ptr("1.19.0"), - utils.Ptr("1.20.0"), - false, - false, - true, - }, - { - "no_matching_available_versions", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.21"), - nil, - nil, - false, - false, - false, - }, - { - "no_matching_available_versions_patch", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.21.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.21.1"), - nil, - nil, - false, - false, - false, - }, - { - "no_matching_available_versions_patch_2", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.21.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr("1.21.1"), - nil, - nil, - false, - false, - false, - }, - { - "no_matching_available_versions_patch_current", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.21.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - nil, - utils.Ptr("1.21.1"), - nil, - false, - false, - false, - }, - { - "no_matching_available_versions_patch_2_current", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.21.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - nil, - utils.Ptr("1.21.1"), - nil, - false, - false, - false, - }, - { - "no_available_version", - []ske.KubernetesVersion{}, - utils.Ptr("1.20"), - nil, - nil, - false, - false, - false, - }, - { - "nil_available_version", - nil, - utils.Ptr("1.20"), - nil, - nil, - false, - false, - false, - }, - { - "empty_provided_version", - []ske.KubernetesVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - utils.Ptr(""), - nil, - nil, - false, - false, - false, - }, - { - description: "minimum_version_without_patch_version_results_in_latest_supported_version,even_if_preview_is_available", - availableVersions: []ske.KubernetesVersion{ - {Version: utils.Ptr("1.20.0"), State: utils.Ptr(VersionStateSupported)}, - {Version: utils.Ptr("1.20.1"), State: utils.Ptr(VersionStateSupported)}, - {Version: utils.Ptr("1.20.2"), State: utils.Ptr(VersionStateSupported)}, - {Version: utils.Ptr("1.20.3"), State: utils.Ptr(VersionStateSupported)}, - {Version: utils.Ptr("1.20.4"), State: utils.Ptr(VersionStatePreview)}, - }, - kubernetesVersionMin: utils.Ptr("1.20"), - currentKubernetesVersion: nil, - expectedVersionUsed: utils.Ptr("1.20.3"), - expectedHasDeprecatedVersion: false, - expectedWarning: false, - isValid: true, - }, - { - description: "use_preview_when_no_supported_release_is_available", - availableVersions: []ske.KubernetesVersion{ - {Version: utils.Ptr("1.19.5"), State: utils.Ptr(VersionStateSupported)}, - {Version: utils.Ptr("1.19.6"), State: utils.Ptr(VersionStateSupported)}, - {Version: utils.Ptr("1.19.7"), State: utils.Ptr(VersionStateSupported)}, - {Version: utils.Ptr("1.20.0"), State: utils.Ptr(VersionStateDeprecated)}, - {Version: utils.Ptr("1.20.1"), State: utils.Ptr(VersionStatePreview)}, - }, - kubernetesVersionMin: utils.Ptr("1.20"), - currentKubernetesVersion: nil, - expectedVersionUsed: utils.Ptr("1.20.1"), - expectedHasDeprecatedVersion: false, - expectedWarning: true, - isValid: true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - var diags diag.Diagnostics - versionUsed, hasDeprecatedVersion, err := latestMatchingKubernetesVersion(tt.availableVersions, tt.kubernetesVersionMin, tt.currentKubernetesVersion, &diags) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - if *versionUsed != *tt.expectedVersionUsed { - t.Fatalf("Used version does not match: expecting %s, got %s", *tt.expectedVersionUsed, *versionUsed) - } - if tt.expectedHasDeprecatedVersion != hasDeprecatedVersion { - t.Fatalf("hasDeprecatedVersion flag is wrong: expecting %t, got %t", tt.expectedHasDeprecatedVersion, hasDeprecatedVersion) - } - } - if hasWarnings := len(diags.Warnings()) > 0; tt.expectedWarning != hasWarnings { - t.Fatalf("Emitted warnings do not match. Expected %t but got %t", tt.expectedWarning, hasWarnings) - } - }) - } -} - -func TestLatestMatchingMachineVersion(t *testing.T) { - tests := []struct { - description string - availableVersions []ske.MachineImage - machineVersionMin *string - machineName string - currentMachineImage *ske.Image - expectedVersionUsed *string - expectedHasDeprecatedVersion bool - isValid bool - }{ - { - "available_version", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20.1"), - "foo", - nil, - utils.Ptr("1.20.1"), - false, - true, - }, - { - "available_version_zero_patch", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20.0"), - "foo", - nil, - utils.Ptr("1.20.0"), - false, - true, - }, - { - "available_version_with_no_provided_patch", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20"), - "foo", - nil, - utils.Ptr("1.20.2"), - false, - true, - }, - { - "available_version_with_higher_preview_patch_not_selected", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.1"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.2"), - State: utils.Ptr(VersionStatePreview), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20"), - "foo", - nil, - utils.Ptr("1.20.1"), - false, - true, - }, - { - "available_version_with_no_provided_patch_2", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20"), - "foo", - nil, - utils.Ptr("1.20.0"), - false, - true, - }, - { - "deprecated_version", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateDeprecated), - }, - }, - }, - }, - utils.Ptr("1.19"), - "foo", - nil, - utils.Ptr("1.19.0"), - true, - true, - }, - { - "preview_version_selected", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStatePreview), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateDeprecated), - }, - }, - }, - }, - utils.Ptr("1.20.0"), - "foo", - nil, - utils.Ptr("1.20.0"), - false, - true, - }, - { - "nil_provided_version_get_latest", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - nil, - "foo", - nil, - utils.Ptr("1.20.0"), - false, - true, - }, - { - "nil_provided_version_use_current", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - nil, - "foo", - &ske.Image{ - Name: utils.Ptr("foo"), - Version: utils.Ptr("1.19.0"), - }, - utils.Ptr("1.19.0"), - false, - true, - }, - { - "nil_provided_version_os_image_update_get_latest", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - nil, - "foo", - &ske.Image{ - Name: utils.Ptr("bar"), - Version: utils.Ptr("1.19.0"), - }, - utils.Ptr("1.20.0"), - false, - true, - }, - { - "update_lower_min_provided", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.19"), - "foo", - &ske.Image{ - Name: utils.Ptr("foo"), - Version: utils.Ptr("1.20.0"), - }, - utils.Ptr("1.20.0"), - false, - true, - }, - { - "update_lower_min_provided_deprecated_version", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.21.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateDeprecated), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.19"), - "foo", - &ske.Image{ - Name: utils.Ptr("foo"), - Version: utils.Ptr("1.20.0"), - }, - utils.Ptr("1.20.0"), - true, - true, - }, - { - "update_higher_min_provided", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20"), - "foo", - &ske.Image{ - Name: utils.Ptr("foo"), - Version: utils.Ptr("1.19.0"), - }, - utils.Ptr("1.20.0"), - false, - true, - }, - { - "no_matching_available_versions", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - { - Version: utils.Ptr("1.19.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.21"), - "foo", - nil, - nil, - false, - false, - }, - { - "no_available_versions", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{}, - }, - }, - utils.Ptr("1.20"), - "foo", - nil, - nil, - false, - false, - }, - { - "nil_available_versions", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: nil, - }, - }, - utils.Ptr("1.20"), - "foo", - nil, - nil, - false, - false, - }, - { - "nil_name", - []ske.MachineImage{ - { - Name: nil, - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20"), - "foo", - nil, - nil, - false, - false, - }, - { - "name_not_available", - []ske.MachineImage{ - { - Name: utils.Ptr("bar"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr("1.20"), - "foo", - nil, - nil, - false, - false, - }, - { - "empty_provided_version", - []ske.MachineImage{ - { - Name: utils.Ptr("foo"), - Versions: &[]ske.MachineImageVersion{ - { - Version: utils.Ptr("1.20.0"), - State: utils.Ptr(VersionStateSupported), - }, - }, - }, - }, - utils.Ptr(""), - "foo", - nil, - nil, - false, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - versionUsed, hasDeprecatedVersion, err := latestMatchingMachineVersion(tt.availableVersions, tt.machineVersionMin, tt.machineName, tt.currentMachineImage) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - if *versionUsed != *tt.expectedVersionUsed { - t.Fatalf("Used version does not match: expecting %s, got %s", *tt.expectedVersionUsed, *versionUsed) - } - if tt.expectedHasDeprecatedVersion != hasDeprecatedVersion { - t.Fatalf("hasDeprecatedVersion flag is wrong: expecting %t, got %t", tt.expectedHasDeprecatedVersion, hasDeprecatedVersion) - } - } - }) - } -} - -func TestGetMaintenanceTimes(t *testing.T) { - tests := []struct { - description string - startAPI time.Time - startTF *string - endAPI time.Time - endTF *string - isValid bool - startExpected string - endExpected string - }{ - { - description: "base", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 7, time.FixedZone("UTC+7:08", 7*60*60+8*60)), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 17, time.FixedZone("UTC+17:18", 17*60*60+18*60)), - isValid: true, - startExpected: "04:05:06+07:08", - endExpected: "14:15:16+17:18", - }, - { - description: "base_utc", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 0, time.UTC), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 0, time.UTC), - isValid: true, - startExpected: "04:05:06Z", - endExpected: "14:15:16Z", - }, - { - description: "tf_state_filled_in_1", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 7, time.FixedZone("UTC+7:08", 7*60*60+8*60)), - startTF: utils.Ptr("04:05:06+07:08"), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 17, time.FixedZone("UTC+17:18", 17*60*60+18*60)), - endTF: utils.Ptr("14:15:16+17:18"), - isValid: true, - startExpected: "04:05:06+07:08", - endExpected: "14:15:16+17:18", - }, - { - description: "tf_state_filled_in_2", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 0, time.UTC), - startTF: utils.Ptr("04:05:06+00:00"), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 0, time.UTC), - endTF: utils.Ptr("14:15:16+00:00"), - isValid: true, - startExpected: "04:05:06+00:00", - endExpected: "14:15:16+00:00", - }, - { - description: "tf_state_filled_in_3", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 0, time.UTC), - startTF: utils.Ptr("04:05:06Z"), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 0, time.UTC), - endTF: utils.Ptr("14:15:16Z"), - isValid: true, - startExpected: "04:05:06Z", - endExpected: "14:15:16Z", - }, - { - description: "api_takes_precedence_if_different_1", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 7, time.FixedZone("UTC+7:08", 7*60*60+8*60)), - startTF: utils.Ptr("00:00:00+07:08"), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 17, time.FixedZone("UTC+17:18", 17*60*60+18*60)), - endTF: utils.Ptr("14:15:16+17:18"), - isValid: true, - startExpected: "04:05:06+07:08", - endExpected: "14:15:16+17:18", - }, - { - description: "api_takes_precedence_if_different_2", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 7, time.FixedZone("UTC+7:08", 7*60*60+8*60)), - startTF: utils.Ptr("04:05:06+07:08"), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 17, time.FixedZone("UTC+17:18", 17*60*60+18*60)), - endTF: utils.Ptr("00:00:00+17:18"), - isValid: true, - startExpected: "04:05:06+07:08", - endExpected: "14:15:16+17:18", - }, - { - description: "api_takes_precedence_if_different_3", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 7, time.FixedZone("UTC+7:08", 7*60*60+8*60)), - startTF: utils.Ptr("04:05:06Z"), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 17, time.FixedZone("UTC+17:18", 17*60*60+18*60)), - endTF: utils.Ptr("14:15:16+17:18"), - isValid: true, - startExpected: "04:05:06+07:08", - endExpected: "14:15:16+17:18", - }, - { - description: "api_takes_precedence_if_different_3", - startAPI: time.Date(1, 2, 3, 4, 5, 6, 7, time.FixedZone("UTC+7:08", 7*60*60+8*60)), - startTF: utils.Ptr("04:05:06+07:08"), - endAPI: time.Date(11, 12, 13, 14, 15, 16, 17, time.FixedZone("UTC+17:18", 17*60*60+18*60)), - endTF: utils.Ptr("14:15:16Z"), - isValid: true, - startExpected: "04:05:06+07:08", - endExpected: "14:15:16+17:18", - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - apiResponse := &ske.Cluster{ - Maintenance: &ske.Maintenance{ - TimeWindow: &ske.TimeWindow{ - Start: utils.Ptr(tt.startAPI), - End: utils.Ptr(tt.endAPI), - }, - }, - } - - maintenanceValues := map[string]attr.Value{ - "enable_kubernetes_version_updates": types.BoolNull(), - "enable_machine_image_version_updates": types.BoolNull(), - "start": types.StringPointerValue(tt.startTF), - "end": types.StringPointerValue(tt.endTF), - } - maintenanceObject, diags := types.ObjectValue(maintenanceTypes, maintenanceValues) - if diags.HasError() { - t.Fatalf("failed to create flavor: %v", core.DiagsToError(diags)) - } - tfState := &Model{ - Maintenance: maintenanceObject, - } - - start, end, err := getMaintenanceTimes(context.Background(), apiResponse, tfState) - - if err != nil { - if tt.isValid { - t.Errorf("getMaintenanceTimes failed on valid input: %v", err) - } - return - } - if !tt.isValid { - t.Fatalf("getMaintenanceTimes didn't fail on invalid input") - } - if tt.startExpected != start { - t.Errorf("expected start '%s', got '%s'", tt.startExpected, start) - } - if tt.endExpected != end { - t.Errorf("expected end '%s', got '%s'", tt.endExpected, end) - } - }) - } -} - -func TestGetCurrentVersion(t *testing.T) { - tests := []struct { - description string - mockedResp *ske.Cluster - expectedKubernetesVersion *string - expectedMachineImages map[string]*ske.Image - getClusterFails bool - }{ - { - "ok", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("v1.0.0"), - }, - Nodepools: &[]ske.Nodepool{ - { - Name: utils.Ptr("foo"), - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: utils.Ptr("foo"), - Version: utils.Ptr("v1.0.0"), - }, - }, - }, - { - Name: utils.Ptr("bar"), - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: utils.Ptr("bar"), - Version: utils.Ptr("v2.0.0"), - }, - }, - }, - }, - }, - utils.Ptr("v1.0.0"), - map[string]*ske.Image{ - "foo": { - Name: utils.Ptr("foo"), - Version: utils.Ptr("v1.0.0"), - }, - "bar": { - Name: utils.Ptr("bar"), - Version: utils.Ptr("v2.0.0"), - }, - }, - false, - }, - { - "get fails", - nil, - nil, - nil, - true, - }, - { - "nil kubernetes", - &ske.Cluster{ - Kubernetes: nil, - }, - nil, - nil, - false, - }, - { - "nil kubernetes version", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: nil, - }, - }, - nil, - nil, - false, - }, - { - "nil nodepools", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("v1.0.0"), - }, - Nodepools: nil, - }, - utils.Ptr("v1.0.0"), - nil, - false, - }, - { - "nil nodepools machine", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("v1.0.0"), - }, - Nodepools: &[]ske.Nodepool{ - { - Name: utils.Ptr("foo"), - Machine: nil, - }, - }, - }, - utils.Ptr("v1.0.0"), - map[string]*ske.Image{}, - false, - }, - { - "nil nodepools machine image", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("v1.0.0"), - }, - Nodepools: &[]ske.Nodepool{ - { - Name: utils.Ptr("foo"), - Machine: &ske.Machine{ - Image: nil, - }, - }, - }, - }, - utils.Ptr("v1.0.0"), - map[string]*ske.Image{}, - false, - }, - { - "nil nodepools machine image name", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("v1.0.0"), - }, - Nodepools: &[]ske.Nodepool{ - { - Name: utils.Ptr("foo"), - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: nil, - }, - }, - }, - }, - }, - utils.Ptr("v1.0.0"), - map[string]*ske.Image{}, - false, - }, - { - "nil nodepools machine image version", - &ske.Cluster{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("v1.0.0"), - }, - Nodepools: &[]ske.Nodepool{ - { - Name: utils.Ptr("foo"), - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: utils.Ptr("foo"), - Version: nil, - }, - }, - }, - }, - }, - utils.Ptr("v1.0.0"), - map[string]*ske.Image{ - "foo": { - Name: utils.Ptr("foo"), - Version: nil, - }, - }, - false, - }, - { - "nil response", - nil, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &skeClientMocked{ - returnError: tt.getClusterFails, - getClusterResp: tt.mockedResp, - } - model := &Model{ - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - } - kubernetesVersion, machineImageVersions := getCurrentVersions(context.Background(), client, model) - diff := cmp.Diff(kubernetesVersion, tt.expectedKubernetesVersion) - if diff != "" { - t.Errorf("Kubernetes version does not match: %s", diff) - } - - diff = cmp.Diff(machineImageVersions, tt.expectedMachineImages) - if diff != "" { - t.Errorf("Machine images do not match: %s", diff) - } - }) - } -} - -func TestGetLatestSupportedKubernetesVersion(t *testing.T) { - tests := []struct { - description string - listKubernetesVersion []ske.KubernetesVersion - isValid bool - expectedVersion *string - }{ - { - description: "base", - listKubernetesVersion: []ske.KubernetesVersion{ - { - State: utils.Ptr("supported"), - Version: utils.Ptr("1.2.3"), - }, - { - State: utils.Ptr("supported"), - Version: utils.Ptr("3.2.1"), - }, - { - State: utils.Ptr("not-supported"), - Version: utils.Ptr("4.4.4"), - }, - }, - isValid: true, - expectedVersion: utils.Ptr("3.2.1"), - }, - { - description: "no Kubernetes versions 1", - listKubernetesVersion: nil, - isValid: false, - }, - { - description: "no Kubernetes versions 2", - listKubernetesVersion: []ske.KubernetesVersion{}, - isValid: false, - }, - { - description: "no supported Kubernetes versions", - listKubernetesVersion: []ske.KubernetesVersion{ - { - State: utils.Ptr("not-supported"), - Version: utils.Ptr("1.2.3"), - }, - }, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - version, err := getLatestSupportedKubernetesVersion(tt.listKubernetesVersion) - - if tt.isValid && err != nil { - t.Errorf("failed on valid input") - } - if !tt.isValid && err == nil { - t.Errorf("did not fail on invalid input") - } - if !tt.isValid { - return - } - diff := cmp.Diff(version, tt.expectedVersion) - if diff != "" { - t.Fatalf("Output is not as expected: %s", diff) - } - }) - } -} - -func TestGetLatestSupportedMachineVersion(t *testing.T) { - tests := []struct { - description string - listMachineVersion []ske.MachineImageVersion - isValid bool - expectedVersion *string - }{ - { - description: "base", - listMachineVersion: []ske.MachineImageVersion{ - { - State: utils.Ptr("supported"), - Version: utils.Ptr("1.2.3"), - }, - { - State: utils.Ptr("supported"), - Version: utils.Ptr("3.2.1"), - }, - { - State: utils.Ptr("not-supported"), - Version: utils.Ptr("4.4.4"), - }, - }, - isValid: true, - expectedVersion: utils.Ptr("3.2.1"), - }, - { - description: "no mchine versions 1", - listMachineVersion: nil, - isValid: false, - }, - { - description: "no machine versions 2", - listMachineVersion: []ske.MachineImageVersion{}, - isValid: false, - }, - { - description: "no supported machine versions", - listMachineVersion: []ske.MachineImageVersion{ - { - State: utils.Ptr("not-supported"), - Version: utils.Ptr("1.2.3"), - }, - }, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - version, err := getLatestSupportedMachineVersion(tt.listMachineVersion) - - if tt.isValid && err != nil { - t.Errorf("failed on valid input") - } - if !tt.isValid && err == nil { - t.Errorf("did not fail on invalid input") - } - if !tt.isValid { - return - } - diff := cmp.Diff(version, tt.expectedVersion) - if diff != "" { - t.Fatalf("Output is not as expected: %s", diff) - } - }) - } -} - -func TestToNetworkPayload(t *testing.T) { - tests := []struct { - description string - model *Model - expected *ske.Network - isValid bool - }{ - { - "base", - &Model{ - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "id": types.StringValue("nid"), - }), - }, - &ske.Network{ - Id: utils.Ptr("nid"), - }, - true, - }, - { - "no_id", - &Model{ - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - Network: types.ObjectValueMust(networkTypes, map[string]attr.Value{ - "id": types.StringNull(), - }), - }, - &ske.Network{}, - true, - }, - { - "no_network", - &Model{ - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - Network: types.ObjectNull(networkTypes), - }, - nil, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - payload, err := toNetworkPayload(context.Background(), tt.model) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid { - diff := cmp.Diff(payload, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestVerifySystemComponentNodepools(t *testing.T) { - tests := []struct { - description string - nodePools []ske.Nodepool - isValid bool - }{ - { - description: "all pools allow system components", - nodePools: []ske.Nodepool{ - { - AllowSystemComponents: conversion.BoolValueToPointer(basetypes.NewBoolValue(true)), - }, - { - AllowSystemComponents: conversion.BoolValueToPointer(basetypes.NewBoolValue(true)), - }, - }, - isValid: true, - }, - { - description: "one pool allows system components", - nodePools: []ske.Nodepool{ - { - AllowSystemComponents: conversion.BoolValueToPointer(basetypes.NewBoolValue(true)), - }, - { - AllowSystemComponents: conversion.BoolValueToPointer(basetypes.NewBoolValue(false)), - }, - }, - isValid: true, - }, - { - description: "no pool allows system components", - nodePools: []ske.Nodepool{ - { - AllowSystemComponents: conversion.BoolValueToPointer(basetypes.NewBoolValue(false)), - }, - { - AllowSystemComponents: conversion.BoolValueToPointer(basetypes.NewBoolValue(false)), - }, - }, - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := verifySystemComponentsInNodePools(tt.nodePools) - if (err == nil) != tt.isValid { - t.Errorf("expected validity to be %v, but got error: %v", tt.isValid, err) - } - }) - } -} - -func TestMaintenanceWindow(t *testing.T) { - tc := []struct { - start string - end string - wantStart string - wantEnd string - }{ - {"01:00:00Z", "02:00:00Z", "01:00:00", "02:00:00"}, - {"01:00:00+00:00", "02:00:00+00:00", "01:00:00", "02:00:00"}, - {"01:00:00+05:00", "02:00:00+05:00", "01:00:00", "02:00:00"}, - {"01:00:00-05:00", "02:00:00-05:00", "01:00:00", "02:00:00"}, - } - for _, tt := range tc { - t.Run(fmt.Sprintf("from %s to %s", tt.start, tt.end), func(t *testing.T) { - attributeTypes := map[string]attr.Type{ - "start": types.StringType, - "end": types.StringType, - "enable_kubernetes_version_updates": types.BoolType, - "enable_machine_image_version_updates": types.BoolType, - } - - attributeValues := map[string]attr.Value{ - "start": basetypes.NewStringValue(tt.start), - "end": basetypes.NewStringValue(tt.end), - "enable_kubernetes_version_updates": basetypes.NewBoolValue(false), - "enable_machine_image_version_updates": basetypes.NewBoolValue(false), - } - - val, diags := basetypes.NewObjectValue(attributeTypes, attributeValues) - if diags.HasError() { - t.Fatalf("cannot create object value: %v", diags) - } - model := Model{ - Maintenance: val, - } - maintenance, err := toMaintenancePayload(context.Background(), &model) - if err != nil { - t.Fatalf("cannot create payload: %v", err) - } - - startLocation := maintenance.TimeWindow.Start.Location() - endLocation := maintenance.TimeWindow.End.Location() - wantStart, err := time.ParseInLocation(time.TimeOnly, tt.wantStart, startLocation) - if err != nil { - t.Fatalf("cannot parse start date %q: %v", tt.wantStart, err) - } - wantEnd, err := time.ParseInLocation(time.TimeOnly, tt.wantEnd, endLocation) - if err != nil { - t.Fatalf("cannot parse end date %q: %v", tt.wantEnd, err) - } - - if expected, actual := wantStart.In(startLocation), *maintenance.TimeWindow.Start; expected != actual { - t.Errorf("invalid start date. expected %s but got %s", expected, actual) - } - if expected, actual := wantEnd.In(endLocation), (*maintenance.TimeWindow.End); expected != actual { - t.Errorf("invalid End date. expected %s but got %s", expected, actual) - } - }) - } -} - -func TestSortK8sVersion(t *testing.T) { - testcases := []struct { - description string - versions []ske.KubernetesVersion - wantSorted []ske.KubernetesVersion - }{ - { - description: "slice with well formed elements", - versions: []ske.KubernetesVersion{ - {Version: utils.Ptr("v1.2.3")}, - {Version: utils.Ptr("v1.1.10")}, - {Version: utils.Ptr("v1.2.1")}, - {Version: utils.Ptr("v1.2.0")}, - {Version: utils.Ptr("v1.1")}, - {Version: utils.Ptr("v1.2.2")}, - }, - wantSorted: []ske.KubernetesVersion{ - {Version: utils.Ptr("v1.2.3")}, - {Version: utils.Ptr("v1.2.2")}, - {Version: utils.Ptr("v1.2.1")}, - {Version: utils.Ptr("v1.2.0")}, - {Version: utils.Ptr("v1.1.10")}, - {Version: utils.Ptr("v1.1")}, - }, - }, - { - description: "slice with undefined elements", - versions: []ske.KubernetesVersion{ - {Version: utils.Ptr("v1.2.3")}, - {Version: utils.Ptr("v1.1.10")}, - {}, - {Version: utils.Ptr("v1.2.0")}, - {Version: utils.Ptr("v1.1")}, - {Version: utils.Ptr("v1.2.2")}, - }, - wantSorted: []ske.KubernetesVersion{ - {Version: utils.Ptr("v1.2.3")}, - {Version: utils.Ptr("v1.2.2")}, - {Version: utils.Ptr("v1.2.0")}, - {Version: utils.Ptr("v1.1.10")}, - {Version: utils.Ptr("v1.1")}, - {Version: nil}, - }, - }, - { - description: "slice without prefix and minor version change", - versions: []ske.KubernetesVersion{ - {Version: utils.Ptr("1.20.0")}, - {Version: utils.Ptr("1.19.0")}, - {Version: utils.Ptr("1.20.1")}, - {Version: utils.Ptr("1.20.2")}, - }, - wantSorted: []ske.KubernetesVersion{ - {Version: utils.Ptr("1.20.2")}, - {Version: utils.Ptr("1.20.1")}, - {Version: utils.Ptr("1.20.0")}, - {Version: utils.Ptr("1.19.0")}, - }, - }, - { - description: "empty slice", - }, - } - for _, tc := range testcases { - t.Run(tc.description, func(t *testing.T) { - sortK8sVersions(tc.versions) - - joinK8sVersions := func(in []ske.KubernetesVersion, sep string) string { - var builder strings.Builder - for i, l := 0, len(in); i < l; i++ { - if i > 0 { - builder.WriteString(sep) - } - if v := in[i].Version; v != nil { - builder.WriteString(*v) - } else { - builder.WriteString("undef") - } - } - return builder.String() - } - - expected := joinK8sVersions(tc.wantSorted, ", ") - actual := joinK8sVersions(tc.versions, ", ") - - if expected != actual { - t.Errorf("wrong sort order. wanted %s but got %s", expected, actual) - } - }) - } -} - -func TestValidateConfig(t *testing.T) { - tests := []struct { - name string - model *Model - wantErr bool - }{ - { - name: "argus and observability null", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectNull(observabilityTypes), - }), - }, - wantErr: false, - }, - { - name: "argus and observability unknown", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectUnknown(argusTypes), - "observability": types.ObjectUnknown(observabilityTypes), - }), - }, - wantErr: false, - }, - { - name: "argus null and observability set", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "instance_id": types.StringValue("aid"), - }), - }), - }, - wantErr: false, - }, - { - name: "argus null and observability unknown", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectNull(argusTypes), - "observability": types.ObjectUnknown(observabilityTypes), - }), - }, - wantErr: false, - }, - { - name: "argus set and observability null", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectValueMust(argusTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "argus_instance_id": types.StringValue("aid"), - }), - "observability": types.ObjectNull(observabilityTypes), - }), - }, - wantErr: false, - }, - { - name: "argus set and observability unknown", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectValueMust(argusTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "argus_instance_id": types.StringValue("aid"), - }), - "observability": types.ObjectUnknown(observabilityTypes), - }), - }, - wantErr: false, - }, - { - name: "argus and observability both set", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectValueMust(argusTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "argus_instance_id": types.StringValue("aid"), - }), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "instance_id": types.StringValue("aid"), - }), - }), - }, - wantErr: true, - }, - { - name: "argus unknown observability null", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectUnknown(argusTypes), - "observability": types.ObjectNull(observabilityTypes), - }), - }, - wantErr: false, - }, - { - name: "argus unknown observability set", - model: &Model{ - Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ - "acl": types.ObjectNull(aclTypes), - "dns": types.ObjectNull(dnsTypes), - "argus": types.ObjectUnknown(argusTypes), - "observability": types.ObjectValueMust(observabilityTypes, map[string]attr.Value{ - "enabled": types.BoolValue(true), - "instance_id": types.StringValue("aid"), - }), - }), - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - validateConfig(ctx, &diags, tt.model) - if diags.HasError() != tt.wantErr { - t.Errorf("validateConfig() = %v, want %v", diags.HasError(), tt.wantErr) - } - }) - } -} diff --git a/stackit/internal/services/ske/kubeconfig/resource.go b/stackit/internal/services/ske/kubeconfig/resource.go deleted file mode 100644 index 0008d5f9..00000000 --- a/stackit/internal/services/ske/kubeconfig/resource.go +++ /dev/null @@ -1,512 +0,0 @@ -package ske - -import ( - "context" - "fmt" - "net/http" - "strconv" - "time" - - skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" - - "github.com/google/uuid" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "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" - sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &kubeconfigResource{} - _ resource.ResourceWithConfigure = &kubeconfigResource{} - _ resource.ResourceWithModifyPlan = &kubeconfigResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - ClusterName types.String `tfsdk:"cluster_name"` - ProjectId types.String `tfsdk:"project_id"` - KubeconfigId types.String `tfsdk:"kube_config_id"` // uuid generated internally because kubeconfig has no identifier - Kubeconfig types.String `tfsdk:"kube_config"` - Expiration types.Int64 `tfsdk:"expiration"` - Refresh types.Bool `tfsdk:"refresh"` - RefreshBefore types.Int64 `tfsdk:"refresh_before"` - ExpiresAt types.String `tfsdk:"expires_at"` - CreationTime types.String `tfsdk:"creation_time"` - Region types.String `tfsdk:"region"` -} - -// NewKubeconfigResource is a helper function to simplify the provider implementation. -func NewKubeconfigResource() resource.Resource { - return &kubeconfigResource{} -} - -// kubeconfigResource is the resource implementation. -type kubeconfigResource struct { - client *ske.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *kubeconfigResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_ske_kubeconfig" -} - -// Configure adds the provider configured client to the resource. -func (r *kubeconfigResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := skeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "SKE kubeconfig client configured") -} - -// Schema defines the schema for the resource. -func (r *kubeconfigResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SKE kubeconfig resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`cluster_name`,`kube_config_id`\".", - "kube_config_id": "Internally generated UUID to identify a kubeconfig resource in Terraform, since the SKE API doesnt return a kubeconfig identifier", - "cluster_name": "Name of the SKE cluster.", - "project_id": "STACKIT project ID to which the cluster is associated.", - "kube_config": "Raw short-lived admin kubeconfig.", - "expiration": "Expiration time of the kubeconfig, in seconds. Defaults to `3600`", - "expires_at": "Timestamp when the kubeconfig expires", - "refresh": "If set to true, the provider will check if the kubeconfig has expired and will generated a new valid one in-place", - "refresh_before": "Number of seconds before expiration to trigger refresh of the kubeconfig at. Only used if refresh is set to true.", - "creation_time": "Date-time when the kubeconfig was created", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "kube_config_id": schema.StringAttribute{ - Description: descriptions["kube_config_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cluster_name": schema.StringAttribute{ - Description: descriptions["cluster_name"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "expiration": schema.Int64Attribute{ - Description: descriptions["expiration"], - Optional: true, - Computed: true, - Default: int64default.StaticInt64(3600), // the default value is not returned by the API so we set a default value here, otherwise we would have to compute the expiration based on the expires_at field - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - int64planmodifier.UseStateForUnknown(), - }, - }, - "refresh": schema.BoolAttribute{ - Description: descriptions["refresh"], - Optional: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - }, - "refresh_before": schema.Int64Attribute{ - Description: descriptions["refresh_before"], - Optional: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - Validators: []validator.Int64{ - int64validator.AtLeast(1), - }, - }, - "kube_config": schema.StringAttribute{ - Description: descriptions["kube_config"], - Computed: true, - Sensitive: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "expires_at": schema.StringAttribute{ - Description: descriptions["expires_at"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "creation_time": schema.StringAttribute{ - Description: descriptions["creation_time"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// ModifyPlan will be called in the Plan phase and will check if the plan is a creation of the resource -// If so, show warning related to deprecated credentials endpoints -func (r *kubeconfigResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - if req.State.Raw.IsNull() { - // Planned to create a kubeconfig - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Planned to create kubeconfig", "Once this resource is created, you will no longer be able to use the deprecated credentials endpoints and the kube_config field on the cluster resource will be empty for this cluster. For more info check How to Rotate SKE Credentials (https://docs.stackit.cloud/products/runtime/kubernetes-engine/how-tos/rotate-ske-credentials)") - } - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - clusterName := model.ClusterName.ValueString() - kubeconfigUUID := uuid.New().String() - region := model.Region.ValueString() - - model.KubeconfigId = types.StringValue(kubeconfigUUID) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "cluster_name", clusterName) - ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID) - ctx = tflog.SetField(ctx, "region", region) - - err := r.createKubeconfig(ctx, &model) - - ctx = core.LogResponse(ctx) - - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating kubeconfig", fmt.Sprintf("Creating kubeconfig: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SKE kubeconfig created") -} - -// Read refreshes the Terraform state with the latest data. -// There is no GET kubeconfig endpoint. -// If the refresh field is set, Read will check the expiration date and will get a new valid kubeconfig if it has expired -// If kubeconfig creation time is before lastCompletionTime of the credentials rotation or -// before cluster creation time a new kubeconfig is created. -func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - clusterName := model.ClusterName.ValueString() - kubeconfigUUID := model.KubeconfigId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - // Prevent error state when updating to v2 api version and the kubeconfig is expired - model.Region = types.StringValue(region) - // Prevent recreation of kubeconfig when updating to v2 api version - diags = resp.State.SetAttribute(ctx, path.Root("region"), region) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "cluster_name", clusterName) - ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID) - ctx = tflog.SetField(ctx, "region", region) - - cluster, err := r.client.GetClusterExecute(ctx, projectId, region, clusterName) - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading kubeconfig", - fmt.Sprintf("Kubeconfig with ID %q or cluster with name %q does not exist in project %q.", kubeconfigUUID, clusterName, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // check if kubeconfig has expired - hasExpired, err := checkHasExpired(&model, time.Now()) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("%v", err)) - return - } - - clusterRecreation, err := checkClusterRecreation(cluster, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("%v", err)) - return - } - - credentialsRotation, err := checkCredentialsRotation(cluster, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("%v", err)) - return - } - - if hasExpired || clusterRecreation || credentialsRotation { - err := r.createKubeconfig(ctx, &model) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading kubeconfig", fmt.Sprintf("The existing kubeconfig is invalid, creating a new one: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - tflog.Info(ctx, "SKE kubeconfig read") -} - -func (r *kubeconfigResource) createKubeconfig(ctx context.Context, model *Model) error { - // Generate API request body from model - payload, err := toCreatePayload(model) - if err != nil { - return fmt.Errorf("creating API payload: %w", err) - } - // Create new kubeconfig - kubeconfigResp, err := r.client.CreateKubeconfig(ctx, model.ProjectId.ValueString(), model.Region.ValueString(), model.ClusterName.ValueString()).CreateKubeconfigPayload(*payload).Execute() - if err != nil { - return fmt.Errorf("calling API: %w", err) - } - - // Map response body to schema - err = mapFields(kubeconfigResp, model, time.Now(), model.Region.ValueString()) - if err != nil { - return fmt.Errorf("processing API payload: %w", err) - } - return nil -} - -func (r *kubeconfigResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating kubeconfig", "Kubeconfig can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *kubeconfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - core.LogAndAddWarning(ctx, &resp.Diagnostics, "Deleting kubeconfig", "Deleted this resource will only remove the values from the terraform state, it will not trigger a deletion or revoke of the actual kubeconfig as this is not supported by the SKE API. The kubeconfig will still be valid until it expires.") - - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - projectId := model.ProjectId.ValueString() - clusterName := model.ClusterName.ValueString() - kubeconfigUUID := model.KubeconfigId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "cluster_name", clusterName) - ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID) - ctx = tflog.SetField(ctx, "region", region) - - // kubeconfig is deleted automatically from the state - tflog.Info(ctx, "SKE kubeconfig deleted") -} - -func mapFields(kubeconfigResp *ske.Kubeconfig, model *Model, creationTime time.Time, region string) error { - if kubeconfigResp == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), model.ClusterName.ValueString(), model.KubeconfigId.ValueString(), - ) - - if kubeconfigResp.Kubeconfig == nil { - return fmt.Errorf("kubeconfig not present") - } - - model.Kubeconfig = types.StringPointerValue(kubeconfigResp.Kubeconfig) - model.ExpiresAt = types.StringValue(kubeconfigResp.ExpirationTimestamp.Format(time.RFC3339)) - // set creation time - model.CreationTime = types.StringValue(creationTime.Format(time.RFC3339)) - model.Region = types.StringValue(region) - return nil -} - -func toCreatePayload(model *Model) (*ske.CreateKubeconfigPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - expiration := conversion.Int64ValueToPointer(model.Expiration) - var expirationStringPtr *string - if expiration != nil { - expirationStringPtr = sdkUtils.Ptr(strconv.FormatInt(*expiration, 10)) - } - - return &ske.CreateKubeconfigPayload{ - ExpirationSeconds: expirationStringPtr, - }, nil -} - -// helper function to check if kubecondig has expired -func checkHasExpired(model *Model, currentTime time.Time) (bool, error) { - expiresAt := model.ExpiresAt - if model.Refresh.ValueBool() && !expiresAt.IsNull() { - if expiresAt.IsUnknown() { - return true, nil - } - expiresAt, err := time.Parse(time.RFC3339, expiresAt.ValueString()) - if err != nil { - return false, fmt.Errorf("converting expiresAt field to timestamp: %w", err) - } - if !model.RefreshBefore.IsNull() { - expiresAt = expiresAt.Add(-time.Duration(model.RefreshBefore.ValueInt64()) * time.Second) - } - if expiresAt.Before(currentTime) { - return true, nil - } - } - return false, nil -} - -// helper function to check if a credentials rotation was done -func checkCredentialsRotation(cluster *ske.Cluster, model *Model) (bool, error) { - creationTimeValue := model.CreationTime - if creationTimeValue.IsNull() || creationTimeValue.IsUnknown() { - return false, nil - } - creationTime, err := time.Parse(time.RFC3339, creationTimeValue.ValueString()) - if err != nil { - return false, fmt.Errorf("converting creationTime field to timestamp: %w", err) - } - if cluster.Status.CredentialsRotation.LastCompletionTime != nil { - if creationTime.Before(*cluster.Status.CredentialsRotation.LastCompletionTime) { - return true, nil - } - } - return false, nil -} - -// helper function to check if a cluster recreation was done -func checkClusterRecreation(cluster *ske.Cluster, model *Model) (bool, error) { - creationTimeValue := model.CreationTime - if creationTimeValue.IsNull() || creationTimeValue.IsUnknown() { - return false, nil - } - creationTime, err := time.Parse(time.RFC3339, creationTimeValue.ValueString()) - if err != nil { - return false, fmt.Errorf("converting creationTime field to timestamp: %w", err) - } - if cluster.Status.CreationTime != nil { - if creationTime.Before(*cluster.Status.CreationTime) { - return true, nil - } - } - return false, nil -} diff --git a/stackit/internal/services/ske/kubeconfig/resource_test.go b/stackit/internal/services/ske/kubeconfig/resource_test.go deleted file mode 100644 index f2a7fac8..00000000 --- a/stackit/internal/services/ske/kubeconfig/resource_test.go +++ /dev/null @@ -1,337 +0,0 @@ -package ske - -import ( - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" -) - -func TestMapFields(t *testing.T) { - const testRegion = "eu01" - tests := []struct { - description string - input *ske.Kubeconfig - expected Model - isValid bool - }{ - { - "simple_values", - &ske.Kubeconfig{ - ExpirationTimestamp: utils.Ptr(time.Date(2024, 2, 7, 16, 42, 12, 0, time.UTC)), - Kubeconfig: utils.Ptr("kubeconfig"), - }, - Model{ - ClusterName: types.StringValue("name"), - ProjectId: types.StringValue("pid"), - Kubeconfig: types.StringValue("kubeconfig"), - Expiration: types.Int64Null(), - Refresh: types.BoolNull(), - RefreshBefore: types.Int64Null(), - ExpiresAt: types.StringValue("2024-02-07T16:42:12Z"), - CreationTime: types.StringValue("2024-02-05T14:40:12Z"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - Model{}, - false, - }, - { - "empty_kubeconfig", - &ske.Kubeconfig{}, - Model{}, - false, - }, - { - "no_kubeconfig_field", - &ske.Kubeconfig{ - ExpirationTimestamp: utils.Ptr(time.Date(2024, 2, 7, 16, 42, 12, 0, time.UTC)), - }, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - ClusterName: tt.expected.ClusterName, - } - creationTime, _ := time.Parse(time.RFC3339, tt.expected.CreationTime.ValueString()) - err := mapFields(tt.input, state, creationTime, testRegion) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected, cmpopts.IgnoreFields(Model{}, "Id")) // Id includes a random uuid - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - expected *ske.CreateKubeconfigPayload - isValid bool - }{ - { - "default_values", - &Model{}, - &ske.CreateKubeconfigPayload{}, - true, - }, - { - "simple_values", - &Model{ - Expiration: types.Int64Value(3600), - }, - &ske.CreateKubeconfigPayload{ - ExpirationSeconds: utils.Ptr("3600"), - }, - true, - }, - { - "nil_model", - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestCheckHasExpired(t *testing.T) { - tests := []struct { - description string - inputModel *Model - currentTime time.Time - expected bool - expectedError bool - }{ - { - description: "has expired", - inputModel: &Model{ - Refresh: types.BoolValue(true), - ExpiresAt: types.StringValue(time.Now().Add(-1 * time.Hour).Format(time.RFC3339)), // one hour ago - }, - currentTime: time.Now(), - expected: true, - expectedError: false, - }, - { - description: "not expired", - inputModel: &Model{ - Refresh: types.BoolValue(true), - ExpiresAt: types.StringValue(time.Now().Add(1 * time.Hour).Format(time.RFC3339)), // in one hour - }, - currentTime: time.Now(), - expected: false, - expectedError: false, - }, - { - description: "refresh is false, expired won't be checked", - inputModel: &Model{ - Refresh: types.BoolValue(false), - ExpiresAt: types.StringValue(time.Now().Add(-1 * time.Hour).Format(time.RFC3339)), // one hour ago - }, - currentTime: time.Now(), - expected: false, - expectedError: false, - }, - { - description: "not expired but refresh_before reached", - inputModel: &Model{ - Refresh: types.BoolValue(true), - RefreshBefore: types.Int64Value(int64(time.Hour.Seconds())), - ExpiresAt: types.StringValue(time.Now().Add(1 * time.Hour).Format(time.RFC3339)), // in one hour - }, - currentTime: time.Now(), - expected: true, - expectedError: false, - }, - { - description: "invalid time", - inputModel: &Model{ - Refresh: types.BoolValue(true), - ExpiresAt: types.StringValue("invalid time"), - }, - currentTime: time.Now(), - expected: false, - expectedError: true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - got, err := checkHasExpired(tt.inputModel, tt.currentTime) - if (err != nil) != tt.expectedError { - t.Errorf("checkHasExpired() error = %v, expectedError %v", err, tt.expectedError) - return - } - if got != tt.expected { - t.Errorf("checkHasExpired() = %v, expected %v", got, tt.expected) - } - }) - } -} - -func TestCheckCredentialsRotation(t *testing.T) { - tests := []struct { - description string - inputCluster *ske.Cluster - inputModel *Model - expected bool - expectedError bool - }{ - { - description: "creation time after credentials rotation", - inputCluster: &ske.Cluster{ - Status: &ske.ClusterStatus{ - CredentialsRotation: &ske.CredentialsRotationState{ - LastCompletionTime: utils.Ptr(time.Now().Add(-1 * time.Hour)), // one hour ago - }, - }, - }, - inputModel: &Model{ - CreationTime: types.StringValue(time.Now().Format(time.RFC3339)), - }, - expected: false, - expectedError: false, - }, - { - description: "creation time before credentials rotation", - inputCluster: &ske.Cluster{ - Status: &ske.ClusterStatus{ - CredentialsRotation: &ske.CredentialsRotationState{ - LastCompletionTime: utils.Ptr(time.Now().Add(1 * time.Hour)), - }, - }, - }, - inputModel: &Model{ - CreationTime: types.StringValue(time.Now().Format(time.RFC3339)), - }, - expected: true, - expectedError: false, - }, - { - description: "last completion time not set", - inputCluster: &ske.Cluster{ - Status: &ske.ClusterStatus{ - CredentialsRotation: &ske.CredentialsRotationState{ - LastCompletionTime: nil, - }, - }, - }, - inputModel: &Model{ - CreationTime: types.StringValue(time.Now().Format(time.RFC3339)), - }, - expected: false, - expectedError: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - got, err := checkCredentialsRotation(tt.inputCluster, tt.inputModel) - if (err != nil) != tt.expectedError { - t.Errorf("checkCredentialsRotation() error = %v, expectedError %v", err, tt.expectedError) - return - } - if got != tt.expected { - t.Errorf("checkCredentialsRotation() = %v, expected %v", got, tt.expected) - } - }) - } -} - -func TestCheckClusterRecreation(t *testing.T) { - tests := []struct { - description string - inputCluster *ske.Cluster - inputModel *Model - expected bool - expectedError bool - }{ - { - description: "cluster creation time after kubeconfig creation time", - inputCluster: &ske.Cluster{ - Status: &ske.ClusterStatus{ - CreationTime: utils.Ptr(time.Now().Add(-1 * time.Hour)), - }, - }, - inputModel: &Model{ - CreationTime: types.StringValue(time.Now().Format(time.RFC3339)), - }, - expected: false, - expectedError: false, - }, - { - description: "cluster creation time before kubeconfig creation time", - inputCluster: &ske.Cluster{ - Status: &ske.ClusterStatus{ - CreationTime: utils.Ptr(time.Now().Add(1 * time.Hour)), - }, - }, - inputModel: &Model{ - CreationTime: types.StringValue(time.Now().Format(time.RFC3339)), - }, - expected: true, - expectedError: false, - }, - { - description: "cluster creation time not set", - inputCluster: &ske.Cluster{ - Status: &ske.ClusterStatus{ - CreationTime: nil, - }, - }, - inputModel: &Model{ - CreationTime: types.StringValue(time.Now().Format(time.RFC3339)), - }, - expected: false, - expectedError: false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - got, err := checkClusterRecreation(tt.inputCluster, tt.inputModel) - if (err != nil) != tt.expectedError { - t.Errorf("checkClusterRecreation() error = %v, expectedError %v", err, tt.expectedError) - return - } - if got != tt.expected { - t.Errorf("checkClusterRecreation() = %v, expected %v", got, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go deleted file mode 100644 index b5a2d179..00000000 --- a/stackit/internal/services/ske/ske_acc_test.go +++ /dev/null @@ -1,614 +0,0 @@ -package ske_test - -import ( - "context" - _ "embed" - "fmt" - "maps" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - coreConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - minTestName = "acc-min" + acctest.RandStringFromCharSet(3, acctest.CharSetAlpha) - maxTestName = "acc-max" + acctest.RandStringFromCharSet(3, acctest.CharSetAlpha) -) - -var ( - //go:embed testdata/resource-min.tf - resourceMin string - - //go:embed testdata/resource-max.tf - resourceMax string -) - -var skeProviderOptions = NewSkeProviderOptions("flatcar") - -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(minTestName), - "nodepool_availability_zone1": config.StringVariable(fmt.Sprintf("%s-1", testutil.Region)), - "nodepool_machine_type": config.StringVariable("g2i.2"), - "nodepool_minimum": config.StringVariable("1"), - "nodepool_maximum": config.StringVariable("2"), - "nodepool_name": config.StringVariable("np-acc-test"), - "kubernetes_version_min": config.StringVariable(skeProviderOptions.GetCreateK8sVersion()), - "maintenance_enable_machine_image_version_updates": config.StringVariable("true"), - "maintenance_enable_kubernetes_version_updates": config.StringVariable("true"), - "maintenance_start": config.StringVariable("02:00:00+01:00"), - "maintenance_end": config.StringVariable("04:00:00+01:00"), - "region": config.StringVariable(testutil.Region), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(maxTestName), - "nodepool_availability_zone1": config.StringVariable(fmt.Sprintf("%s-1", testutil.Region)), - "nodepool_machine_type": config.StringVariable("g2i.2"), - "nodepool_minimum": config.StringVariable("1"), - "nodepool_maximum": config.StringVariable("2"), - "nodepool_name": config.StringVariable("np-acc-test"), - "nodepool_allow_system_components": config.StringVariable("true"), - "nodepool_cri": config.StringVariable("containerd"), - "nodepool_label_value": config.StringVariable("value"), - "nodepool_max_surge": config.StringVariable("1"), - "nodepool_max_unavailable": config.StringVariable("1"), - "nodepool_os_name": config.StringVariable(skeProviderOptions.nodePoolOsName), - "nodepool_os_version_min": config.StringVariable(skeProviderOptions.GetCreateMachineVersion()), - "nodepool_taints_effect": config.StringVariable("PreferNoSchedule"), - "nodepool_taints_key": config.StringVariable("tkey"), - "nodepool_taints_value": config.StringVariable("tvalue"), - "nodepool_volume_size": config.StringVariable("20"), - "nodepool_volume_type": config.StringVariable("storage_premium_perf0"), - "ext_acl_enabled": config.StringVariable("true"), - "ext_acl_allowed_cidr1": config.StringVariable("10.0.100.0/24"), - "ext_observability_enabled": config.StringVariable("false"), - "ext_dns_enabled": config.StringVariable("true"), - "nodepool_hibernations1_start": config.StringVariable("0 18 * * *"), - "nodepool_hibernations1_end": config.StringVariable("59 23 * * *"), - "nodepool_hibernations1_timezone": config.StringVariable("Europe/Berlin"), - "kubernetes_version_min": config.StringVariable(skeProviderOptions.GetCreateK8sVersion()), - "maintenance_enable_machine_image_version_updates": config.StringVariable("true"), - "maintenance_enable_kubernetes_version_updates": config.StringVariable("true"), - "maintenance_start": config.StringVariable("02:00:00+01:00"), - "maintenance_end": config.StringVariable("04:00:00+01:00"), - "region": config.StringVariable(testutil.Region), - "expiration": config.StringVariable("3600"), - "refresh": config.StringVariable("true"), - "refresh_before": config.StringVariable("600"), - "dns_zone_name": config.StringVariable("acc-" + acctest.RandStringFromCharSet(6, acctest.CharSetAlpha)), - "dns_name": config.StringVariable("acc-" + acctest.RandStringFromCharSet(6, acctest.CharSetAlpha) + ".runs.onstackit.cloud"), -} - -func configVarsMinUpdated() config.Variables { - updatedConfig := maps.Clone(testConfigVarsMin) - updatedConfig["kubernetes_version_min"] = config.StringVariable(skeProviderOptions.GetUpdateK8sVersion()) - - return updatedConfig -} - -func configVarsMaxUpdated() config.Variables { - updatedConfig := maps.Clone(testConfigVarsMax) - updatedConfig["kubernetes_version_min"] = config.StringVariable(skeProviderOptions.GetUpdateK8sVersion()) - updatedConfig["nodepool_os_version_min"] = config.StringVariable(skeProviderOptions.GetUpdateMachineVersion()) - updatedConfig["maintenance_end"] = config.StringVariable("03:03:03+00:00") - - return updatedConfig -} - -func TestAccSKEMin(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSKEDestroy, - Steps: []resource.TestStep{ - - // 1) Creation - { - Config: testutil.SKEProviderConfig() + "\n" + resourceMin, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // cluster data - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_availability_zone1"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_machine_type"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_maximum"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_minimum"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_name"])), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "node_pools.0.os_version_used"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_kubernetes_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_machine_image_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_end"])), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "region"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"), - - // Kubeconfig - resource.TestCheckResourceAttrPair( - "stackit_ske_kubeconfig.kubeconfig", "project_id", - "stackit_ske_cluster.cluster", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_ske_kubeconfig.kubeconfig", "cluster_name", - "stackit_ske_cluster.cluster", "name", - ), - ), - }, - // 2) Data source - { - Config: resourceMin, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - - // cluster data - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "id", fmt.Sprintf("%s,%s,%s", - testutil.ConvertConfigVariable(testConfigVarsMin["project_id"]), - testutil.Region, - testutil.ConvertConfigVariable(testConfigVarsMin["name"]), - )), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_availability_zone1"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_machine_type"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_maximum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_minimum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_name"])), - - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_kubernetes_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_machine_image_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_end"])), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "region"), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - ), - }, - // 3) Import cluster - { - ResourceName: "stackit_ske_cluster.cluster", - ConfigVariables: testConfigVarsMin, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_ske_cluster.cluster"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_ske_cluster.cluster") - } - _, ok = r.Primary.Attributes["project_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute project_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, name), nil - }, - ImportState: true, - ImportStateVerify: true, - // The fields are not provided in the SKE API when disabled, although set actively. - ImportStateVerifyIgnore: []string{"kubernetes_version_min", "node_pools.0.os_version_min", "extensions.observability.%", "extensions.observability.instance_id", "extensions.observability.enabled"}, - }, - // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version - { - Config: resourceMin, - ConfigVariables: configVarsMinUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // cluster data - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_availability_zone1"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_machine_type"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_maximum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_minimum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMin["nodepool_name"])), - - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_kubernetes_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_machine_image_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_end"])), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "region"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccSKEMax(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSKEDestroy, - Steps: []resource.TestStep{ - - // 1) Creation - { - Config: testutil.SKEProviderConfig() + "\n" + resourceMax, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // cluster data - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_availability_zone1"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_machine_type"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_maximum"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_minimum"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_name"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.allow_system_components", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_allow_system_components"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.cri", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_cri"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.labels.label_key", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_label_value"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.max_surge", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_max_surge"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.max_unavailable", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_max_unavailable"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_name", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_os_name"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_min", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_os_version_min"])), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "node_pools.0.os_version_used"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.0.effect", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_taints_effect"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.0.key", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_taints_key"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.0.value", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_taints_value"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.volume_size", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_volume_size"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.volume_type", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_volume_type"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.acl.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["ext_acl_enabled"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.acl.allowed_cidrs.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.acl.allowed_cidrs.0", testutil.ConvertConfigVariable(testConfigVarsMax["ext_acl_allowed_cidr1"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.observability.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["ext_observability_enabled"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.dns.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["ext_dns_enabled"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.dns.zones.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.dns.zones.0", testutil.ConvertConfigVariable(testConfigVarsMax["dns_name"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.0.start", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_hibernations1_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.0.end", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_hibernations1_end"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.0.timezone", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_hibernations1_timezone"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_min", testutil.ConvertConfigVariable(testConfigVarsMax["kubernetes_version_min"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_kubernetes_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_machine_image_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_end"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "region", testutil.ConvertConfigVariable(testConfigVarsMax["region"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "egress_address_ranges.#", "1"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "egress_address_ranges.0"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "pod_address_ranges.#", "1"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "pod_address_ranges.0"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"), - - // Kubeconfig - resource.TestCheckResourceAttrPair( - "stackit_ske_kubeconfig.kubeconfig", "project_id", - "stackit_ske_cluster.cluster", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_ske_kubeconfig.kubeconfig", "cluster_name", - "stackit_ske_cluster.cluster", "name", - ), - resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "expiration", testutil.ConvertConfigVariable(testConfigVarsMax["expiration"])), - resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "refresh", testutil.ConvertConfigVariable(testConfigVarsMax["refresh"])), - resource.TestCheckResourceAttr("stackit_ske_kubeconfig.kubeconfig", "refresh_before", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_before"])), - resource.TestCheckResourceAttrSet("stackit_ske_kubeconfig.kubeconfig", "expires_at"), - ), - }, - // 2) Data source - { - Config: resourceMax, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - - // cluster data - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "id", fmt.Sprintf("%s,%s,%s", - testutil.ConvertConfigVariable(testConfigVarsMax["project_id"]), - testutil.Region, - testutil.ConvertConfigVariable(testConfigVarsMax["name"]), - )), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_availability_zone1"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_machine_type"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_maximum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_minimum"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_name"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.allow_system_components", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_allow_system_components"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.cri", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_cri"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.labels.label_key", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_label_value"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.max_surge", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_max_surge"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.max_unavailable", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_max_unavailable"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.os_name", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_os_name"])), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "node_pools.0.os_version_used"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.taints.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.taints.0.effect", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_taints_effect"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.taints.0.key", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_taints_key"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.taints.0.value", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_taints_value"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.volume_size", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_volume_size"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "node_pools.0.volume_type", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_volume_type"])), - - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "extensions.acl.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["ext_acl_enabled"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "extensions.acl.allowed_cidrs.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "extensions.acl.allowed_cidrs.0", testutil.ConvertConfigVariable(testConfigVarsMax["ext_acl_allowed_cidr1"])), - // no check for observability, as it was disabled in the setup - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "extensions.dns.enabled", testutil.ConvertConfigVariable(testConfigVarsMax["ext_dns_enabled"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "extensions.dns.zones.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "extensions.dns.zones.0", testutil.ConvertConfigVariable(testConfigVarsMax["dns_name"])), - - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "hibernations.#", "1"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "hibernations.0.start", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_hibernations1_start"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "hibernations.0.end", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_hibernations1_end"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "hibernations.0.timezone", testutil.ConvertConfigVariable(testConfigVarsMax["nodepool_hibernations1_timezone"])), - - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_kubernetes_version_updates"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_enable_machine_image_version_updates"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_start"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(testConfigVarsMax["maintenance_end"])), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "region", testutil.ConvertConfigVariable(testConfigVarsMax["region"])), - - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "egress_address_ranges.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "egress_address_ranges.0"), - resource.TestCheckResourceAttr("data.stackit_ske_cluster.cluster", "pod_address_ranges.#", "1"), - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "pod_address_ranges.0"), - - resource.TestCheckResourceAttrSet("data.stackit_ske_cluster.cluster", "kubernetes_version_used"), - ), - }, - // 3) Import cluster - { - ResourceName: "stackit_ske_cluster.cluster", - ConfigVariables: testConfigVarsMax, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_ske_cluster.cluster"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_ske_cluster.cluster") - } - _, ok = r.Primary.Attributes["project_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute project_id") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return "", fmt.Errorf("couldn't find attribute name") - } - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, name), nil - }, - ImportState: true, - ImportStateVerify: true, - // The fields are not provided in the SKE API when disabled, although set actively. - ImportStateVerifyIgnore: []string{"kubernetes_version_min", "node_pools.0.os_version_min", "extensions.observability.%", "extensions.observability.instance_id", "extensions.observability.enabled"}, - }, - // 4) Update kubernetes version, OS version and maintenance end, downgrade of kubernetes version - { - Config: resourceMax, - ConfigVariables: configVarsMaxUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // cluster data - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["name"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.availability_zones.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_availability_zone1"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.machine_type", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_machine_type"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.maximum", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_maximum"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.minimum", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_minimum"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_name"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.allow_system_components", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_allow_system_components"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.cri", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_cri"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.labels.label_key", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_label_value"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.max_surge", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_max_surge"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.max_unavailable", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_max_unavailable"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_os_name"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.os_version_min", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_os_version_min"])), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "node_pools.0.os_version_used"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.0.effect", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_taints_effect"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.0.key", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_taints_key"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.taints.0.value", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_taints_value"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.volume_size", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_volume_size"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "node_pools.0.volume_type", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_volume_type"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.acl.enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["ext_acl_enabled"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.acl.allowed_cidrs.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.acl.allowed_cidrs.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["ext_acl_allowed_cidr1"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.observability.enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["ext_observability_enabled"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.dns.enabled", testutil.ConvertConfigVariable(configVarsMaxUpdated()["ext_dns_enabled"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.dns.zones.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "extensions.dns.zones.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["dns_name"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.#", "1"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.0.start", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_hibernations1_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.0.end", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_hibernations1_end"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "hibernations.0.timezone", testutil.ConvertConfigVariable(configVarsMaxUpdated()["nodepool_hibernations1_timezone"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "kubernetes_version_min", testutil.ConvertConfigVariable(configVarsMaxUpdated()["kubernetes_version_min"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_kubernetes_version_updates", testutil.ConvertConfigVariable(configVarsMaxUpdated()["maintenance_enable_kubernetes_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.enable_machine_image_version_updates", testutil.ConvertConfigVariable(configVarsMaxUpdated()["maintenance_enable_machine_image_version_updates"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.start", testutil.ConvertConfigVariable(configVarsMaxUpdated()["maintenance_start"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "maintenance.end", testutil.ConvertConfigVariable(configVarsMaxUpdated()["maintenance_end"])), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "region", testutil.ConvertConfigVariable(configVarsMaxUpdated()["region"])), - - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "egress_address_ranges.#", "1"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "egress_address_ranges.0"), - resource.TestCheckResourceAttr("stackit_ske_cluster.cluster", "pod_address_ranges.#", "1"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "pod_address_ranges.0"), - resource.TestCheckResourceAttrSet("stackit_ske_cluster.cluster", "kubernetes_version_used"), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccCheckSKEDestroy(s *terraform.State) error { - ctx := context.Background() - var client *ske.APIClient - var err error - if testutil.SKECustomEndpoint == "" { - client, err = ske.NewAPIClient() - } else { - client, err = ske.NewAPIClient( - coreConfig.WithEndpoint(testutil.SKECustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - clustersToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_ske_cluster" { - continue - } - // cluster terraform ID: = "[project_id],[region],[cluster_name]" - clusterName := strings.Split(rs.Primary.ID, core.Separator)[2] - clustersToDestroy = append(clustersToDestroy, clusterName) - } - - clustersResp, err := client.ListClusters(ctx, testutil.ProjectId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting clustersResp: %w", err) - } - - items := *clustersResp.Items - for i := range items { - if items[i].Name == nil { - continue - } - if utils.Contains(clustersToDestroy, *items[i].Name) { - _, err := client.DeleteClusterExecute(ctx, testutil.ProjectId, testutil.Region, *items[i].Name) - if err != nil { - return fmt.Errorf("destroying cluster %s during CheckDestroy: %w", *items[i].Name, err) - } - _, err = wait.DeleteClusterWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, *items[i].Name).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying cluster %s during CheckDestroy: waiting for deletion %w", *items[i].Name, err) - } - } - } - return nil -} - -type SkeProviderOptions struct { - options *ske.ProviderOptions - nodePoolOsName string -} - -// NewSkeProviderOptions fetches the latest available options from SKE. -func NewSkeProviderOptions(nodePoolOs string) *SkeProviderOptions { - // skip if TF_ACC=1 is not set - if !testutil.E2ETestsEnabled { - return &SkeProviderOptions{ - options: nil, - nodePoolOsName: nodePoolOs, - } - } - - ctx := context.Background() - - var client *ske.APIClient - var err error - - if testutil.SKECustomEndpoint == "" { - client, err = ske.NewAPIClient() - } else { - client, err = ske.NewAPIClient(coreConfig.WithEndpoint(testutil.SKECustomEndpoint)) - } - - if err != nil { - panic("failed to create SKE client: " + err.Error()) - } - - options, err := client.ListProviderOptions(ctx, testutil.Region).Execute() - if err != nil { - panic("failed to fetch SKE provider options: " + err.Error()) - } - - return &SkeProviderOptions{ - options: options, - nodePoolOsName: nodePoolOs, - } -} - -// getMachineVersionAt returns the N-th supported version for the specified machine image. -func (s *SkeProviderOptions) getMachineVersionAt(position int) string { - // skip if TF_ACC=1 is not set - if !testutil.E2ETestsEnabled { - return "" - } - - if s.options == nil || s.options.MachineImages == nil { - panic(fmt.Sprintf("no supported machine version found at position %d", position)) - } - - for _, mi := range *s.options.MachineImages { - if mi.Name != nil && *mi.Name == s.nodePoolOsName && mi.Versions != nil { - count := 0 - for _, v := range *mi.Versions { - if v.State != nil && v.Version != nil { - if count == position { - return *v.Version - } - count++ - } - } - } - } - - panic(fmt.Sprintf("no supported machine version found at position %d", position)) -} - -// getK8sVersionAt returns the N-th supported Kubernetes version. -func (s *SkeProviderOptions) getK8sVersionAt(position int) string { - // skip if TF_ACC=1 is not set - if !testutil.E2ETestsEnabled { - return "" - } - - if s.options == nil || s.options.KubernetesVersions == nil { - panic(fmt.Sprintf("no supported k8s version found at position %d", position)) - } - - count := 0 - for _, v := range *s.options.KubernetesVersions { - if v.State != nil && *v.State == "supported" && v.Version != nil { - if count == position { - return *v.Version - } - count++ - } - } - - panic(fmt.Sprintf("no supported k8s version found at position %d", position)) -} - -func (s *SkeProviderOptions) GetCreateMachineVersion() string { - return s.getMachineVersionAt(0) -} - -func (s *SkeProviderOptions) GetUpdateMachineVersion() string { - return s.getMachineVersionAt(1) -} - -func (s *SkeProviderOptions) GetCreateK8sVersion() string { - return s.getK8sVersionAt(0) -} - -func (s *SkeProviderOptions) GetUpdateK8sVersion() string { - return s.getK8sVersionAt(1) -} diff --git a/stackit/internal/services/ske/testdata/resource-max.tf b/stackit/internal/services/ske/testdata/resource-max.tf deleted file mode 100644 index 192c9138..00000000 --- a/stackit/internal/services/ske/testdata/resource-max.tf +++ /dev/null @@ -1,116 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "nodepool_availability_zone1" {} -variable "nodepool_machine_type" {} -variable "nodepool_maximum" {} -variable "nodepool_minimum" {} -variable "nodepool_name" {} -variable "nodepool_allow_system_components" {} -variable "nodepool_cri" {} -variable "nodepool_label_value" {} -variable "nodepool_max_surge" {} -variable "nodepool_max_unavailable" {} -variable "nodepool_os_name" {} -variable "nodepool_os_version_min" {} -variable "nodepool_taints_effect" {} -variable "nodepool_taints_key" {} -variable "nodepool_taints_value" {} -variable "nodepool_volume_size" {} -variable "nodepool_volume_type" {} -variable "ext_acl_enabled" {} -variable "ext_acl_allowed_cidr1" {} -variable "ext_observability_enabled" {} -variable "ext_dns_enabled" {} -variable "nodepool_hibernations1_start" {} -variable "nodepool_hibernations1_end" {} -variable "nodepool_hibernations1_timezone" {} -variable "kubernetes_version_min" {} -variable "maintenance_enable_kubernetes_version_updates" {} -variable "maintenance_enable_machine_image_version_updates" {} -variable "maintenance_start" {} -variable "maintenance_end" {} -variable "region" {} -variable "expiration" {} -variable "refresh" {} -variable "refresh_before" {} -variable "dns_zone_name" {} -variable "dns_name" {} - -resource "stackit_ske_cluster" "cluster" { - project_id = var.project_id - name = var.name - - node_pools = [{ - availability_zones = [var.nodepool_availability_zone1] - machine_type = var.nodepool_machine_type - maximum = var.nodepool_maximum - minimum = var.nodepool_minimum - name = var.nodepool_name - - allow_system_components = var.nodepool_allow_system_components - cri = var.nodepool_cri - labels = { - "label_key" = var.nodepool_label_value - } - max_surge = var.nodepool_max_surge - max_unavailable = var.nodepool_max_unavailable - os_name = var.nodepool_os_name - os_version_min = var.nodepool_os_version_min - taints = [{ - effect = var.nodepool_taints_effect - key = var.nodepool_taints_key - value = var.nodepool_taints_value - }] - volume_size = var.nodepool_volume_size - volume_type = var.nodepool_volume_type - } - ] - - extensions = { - acl = { - enabled = var.ext_acl_enabled - allowed_cidrs = [var.ext_acl_allowed_cidr1] - } - observability = { - enabled = var.ext_observability_enabled - } - dns = { - enabled = var.ext_dns_enabled - zones = [stackit_dns_zone.dns-zone.dns_name] - } - } - hibernations = [{ - start = var.nodepool_hibernations1_start - end = var.nodepool_hibernations1_end - timezone = var.nodepool_hibernations1_timezone - }] - kubernetes_version_min = var.kubernetes_version_min - maintenance = { - enable_kubernetes_version_updates = var.maintenance_enable_kubernetes_version_updates - enable_machine_image_version_updates = var.maintenance_enable_machine_image_version_updates - start = var.maintenance_start - end = var.maintenance_end - } - region = var.region -} - -resource "stackit_ske_kubeconfig" "kubeconfig" { - project_id = stackit_ske_cluster.cluster.project_id - cluster_name = stackit_ske_cluster.cluster.name - expiration = var.expiration - refresh = var.refresh - refresh_before = var.refresh_before -} - -data "stackit_ske_cluster" "cluster" { - project_id = var.project_id - name = stackit_ske_cluster.cluster.name -} - - -resource "stackit_dns_zone" "dns-zone" { - project_id = var.project_id - name = var.dns_zone_name - dns_name = var.dns_name -} - diff --git a/stackit/internal/services/ske/testdata/resource-min.tf b/stackit/internal/services/ske/testdata/resource-min.tf deleted file mode 100644 index 776e7fc5..00000000 --- a/stackit/internal/services/ske/testdata/resource-min.tf +++ /dev/null @@ -1,51 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "nodepool_availability_zone1" {} -variable "nodepool_machine_type" {} -variable "nodepool_maximum" {} -variable "nodepool_minimum" {} -variable "nodepool_name" {} -variable "kubernetes_version_min" {} -variable "maintenance_enable_kubernetes_version_updates" {} -variable "maintenance_enable_machine_image_version_updates" {} -variable "maintenance_start" {} -variable "maintenance_end" {} -variable "region" {} - - -resource "stackit_ske_cluster" "cluster" { - project_id = var.project_id - name = var.name - - node_pools = [{ - availability_zones = [var.nodepool_availability_zone1] - machine_type = var.nodepool_machine_type - maximum = var.nodepool_maximum - minimum = var.nodepool_minimum - name = var.nodepool_name - } - ] - kubernetes_version_min = var.kubernetes_version_min - # even though the maintenance attribute is not mandatory, - # it is required for a consistent plan - # see https://jira.schwarz/browse/STACKITTPR-242 - maintenance = { - enable_kubernetes_version_updates = var.maintenance_enable_kubernetes_version_updates - enable_machine_image_version_updates = var.maintenance_enable_machine_image_version_updates - start = var.maintenance_start - end = var.maintenance_end - } - region = var.region -} - -resource "stackit_ske_kubeconfig" "kubeconfig" { - project_id = stackit_ske_cluster.cluster.project_id - cluster_name = stackit_ske_cluster.cluster.name -} - -data "stackit_ske_cluster" "cluster" { - project_id = var.project_id - name = stackit_ske_cluster.cluster.name -} - - diff --git a/stackit/internal/services/ske/utils/util.go b/stackit/internal/services/ske/utils/util.go deleted file mode 100644 index 91e89b17..00000000 --- a/stackit/internal/services/ske/utils/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *ske.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.SKECustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.SKECustomEndpoint)) - } - apiClient, err := ske.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/ske/utils/util_test.go b/stackit/internal/services/ske/utils/util_test.go deleted file mode 100644 index 501406aa..00000000 --- a/stackit/internal/services/ske/utils/util_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://ske-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *ske.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *ske.APIClient { - apiClient, err := ske.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - SKECustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *ske.APIClient { - apiClient, err := ske.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/internal/services/sqlserverflex/instance/datasource.go b/stackit/internal/services/sqlserverflex/instance/datasource.go deleted file mode 100644 index 8ad7afc9..00000000 --- a/stackit/internal/services/sqlserverflex/instance/datasource.go +++ /dev/null @@ -1,238 +0,0 @@ -package sqlserverflex - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &instanceDataSource{} -) - -// NewInstanceDataSource is a helper function to simplify the provider implementation. -func NewInstanceDataSource() datasource.DataSource { - return &instanceDataSource{} -} - -// instanceDataSource is the data source implementation. -type instanceDataSource struct { - client *sqlserverflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *instanceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_sqlserverflex_instance" -} - -// Configure adds the provider configured client to the data source. -func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "SQLServer Flex instance client configured") -} - -// Schema defines the schema for the data source. -func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SQLServer Flex instance data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`\".", - "instance_id": "ID of the SQLServer Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acl": "The Access Control List (ACL) for the SQLServer Flex instance.", - "backup_schedule": `The backup schedule. Should follow the cron scheduling system format (e.g. "0 0 * * *").`, - "options": "Custom parameters for the SQLServer Flex instance.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Computed: true, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Computed: true, - }, - "backup_schedule": schema.StringAttribute{ - Description: descriptions["backup_schedule"], - Computed: true, - }, - "flavor": schema.SingleNestedAttribute{ - Computed: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - }, - "description": schema.StringAttribute{ - Computed: true, - }, - "cpu": schema.Int64Attribute{ - Computed: true, - }, - "ram": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "replicas": schema.Int64Attribute{ - Computed: true, - }, - "storage": schema.SingleNestedAttribute{ - Computed: true, - Attributes: map[string]schema.Attribute{ - "class": schema.StringAttribute{ - Computed: true, - }, - "size": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "version": schema.StringAttribute{ - Computed: true, - }, - "options": schema.SingleNestedAttribute{ - Description: descriptions["options"], - Computed: true, - Attributes: map[string]schema.Attribute{ - "edition": schema.StringAttribute{ - Computed: true, - }, - "retention_days": schema.Int64Attribute{ - Computed: true, - }, - }, - }, - "region": schema.StringAttribute{ - // the region cannot be found, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId, region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading instance", - fmt.Sprintf("Instance with ID %q does not exist in project %q.", instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - err = mapFields(ctx, instanceResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SQLServer Flex instance read") -} diff --git a/stackit/internal/services/sqlserverflex/instance/resource.go b/stackit/internal/services/sqlserverflex/instance/resource.go deleted file mode 100644 index 72a0de30..00000000 --- a/stackit/internal/services/sqlserverflex/instance/resource.go +++ /dev/null @@ -1,899 +0,0 @@ -package sqlserverflex - -import ( - "context" - "fmt" - "net/http" - "regexp" - "strconv" - "strings" - "time" - - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - coreUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/wait" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &instanceResource{} - _ resource.ResourceWithConfigure = &instanceResource{} - _ resource.ResourceWithImportState = &instanceResource{} - _ resource.ResourceWithModifyPlan = &instanceResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Name types.String `tfsdk:"name"` - ACL types.List `tfsdk:"acl"` - BackupSchedule types.String `tfsdk:"backup_schedule"` - Flavor types.Object `tfsdk:"flavor"` - Storage types.Object `tfsdk:"storage"` - Version types.String `tfsdk:"version"` - Replicas types.Int64 `tfsdk:"replicas"` - Options types.Object `tfsdk:"options"` - Region types.String `tfsdk:"region"` -} - -// Struct corresponding to Model.Flavor -type flavorModel struct { - Id types.String `tfsdk:"id"` - Description types.String `tfsdk:"description"` - CPU types.Int64 `tfsdk:"cpu"` - RAM types.Int64 `tfsdk:"ram"` -} - -// Types corresponding to flavorModel -var flavorTypes = map[string]attr.Type{ - "id": basetypes.StringType{}, - "description": basetypes.StringType{}, - "cpu": basetypes.Int64Type{}, - "ram": basetypes.Int64Type{}, -} - -// Struct corresponding to Model.Storage -type storageModel struct { - Class types.String `tfsdk:"class"` - Size types.Int64 `tfsdk:"size"` -} - -// Types corresponding to storageModel -var storageTypes = map[string]attr.Type{ - "class": basetypes.StringType{}, - "size": basetypes.Int64Type{}, -} - -// Struct corresponding to Model.Options -type optionsModel struct { - Edition types.String `tfsdk:"edition"` - RetentionDays types.Int64 `tfsdk:"retention_days"` -} - -// Types corresponding to optionsModel -var optionsTypes = map[string]attr.Type{ - "edition": basetypes.StringType{}, - "retention_days": basetypes.Int64Type{}, -} - -// NewInstanceResource is a helper function to simplify the provider implementation. -func NewInstanceResource() resource.Resource { - return &instanceResource{} -} - -// instanceResource is the resource implementation. -type instanceResource struct { - client *sqlserverflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_sqlserverflex_instance" -} - -// Configure adds the provider configured client to the resource. -func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "SQLServer Flex instance client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Schema defines the schema for the resource. -func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SQLServer Flex instance resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`\".", - "instance_id": "ID of the SQLServer Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "name": "Instance name.", - "acl": "The Access Control List (ACL) for the SQLServer Flex instance.", - "backup_schedule": `The backup schedule. Should follow the cron scheduling system format (e.g. "0 0 * * *")`, - "options": "Custom parameters for the SQLServer Flex instance.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "name": schema.StringAttribute{ - Description: descriptions["name"], - Required: true, - Validators: []validator.String{ - stringvalidator.LengthAtLeast(1), - stringvalidator.RegexMatches( - regexp.MustCompile("^[a-z]([-a-z0-9]*[a-z0-9])?$"), - "must start with a letter, must have lower case letters, numbers or hyphens, and no hyphen at the end", - ), - }, - }, - "acl": schema.ListAttribute{ - Description: descriptions["acl"], - ElementType: types.StringType, - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.List{ - listplanmodifier.UseStateForUnknown(), - }, - }, - "backup_schedule": schema.StringAttribute{ - Description: descriptions["backup_schedule"], - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "flavor": schema.SingleNestedAttribute{ - Required: true, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "description": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "cpu": schema.Int64Attribute{ - Required: true, - }, - "ram": schema.Int64Attribute{ - Required: true, - }, - }, - }, - "replicas": schema.Int64Attribute{ - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - "storage": schema.SingleNestedAttribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.RequiresReplace(), - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "class": schema.StringAttribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "size": schema.Int64Attribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - int64planmodifier.UseStateForUnknown(), - }, - }, - }, - }, - "version": schema.StringAttribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "options": schema.SingleNestedAttribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Object{ - objectplanmodifier.RequiresReplace(), - objectplanmodifier.UseStateForUnknown(), - }, - Attributes: map[string]schema.Attribute{ - "edition": schema.StringAttribute{ - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "retention_days": schema.Int64Attribute{ - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - int64planmodifier.UseStateForUnknown(), - }, - }, - }, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "region", region) - - var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - err := loadFlavorId(ctx, r.client, &model, flavor) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err)) - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, acl, flavor, storage, options) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new instance - createResp, err := r.client.CreateInstance(ctx, projectId, region).CreateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - instanceId := *createResp.Id - ctx = tflog.SetField(ctx, "instance_id", instanceId) - // The creation waiter sometimes returns an error from the API: "instance with id xxx has unexpected status Failure" - // which can be avoided by sleeping before wait - waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).SetSleepBeforeWait(30 * time.Second).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // After the instance creation, database might not be ready to accept connections immediately. - // That is why we add a sleep - time.Sleep(120 * time.Second) - - tflog.Info(ctx, "SQLServer Flex instance created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId, region).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(ctx, instanceResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SQLServer Flex instance read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { - diags = model.ACL.ElementsAs(ctx, &acl, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { - diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - err := loadFlavorId(ctx, r.client, &model, flavor) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err)) - return - } - } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { - diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { - diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toUpdatePayload(&model, acl, flavor) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Update existing instance - _, err = r.client.PartialUpdateInstance(ctx, projectId, instanceId, region).PartialUpdateInstancePayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) - return - } - - ctx = core.LogResponse(ctx) - - waitResp, err := wait.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) - return - } - - // Map response body to schema - err = mapFields(ctx, waitResp, &model, flavor, storage, options, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) - return - } - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SQLServer Flex instance updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from state - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing instance - err := r.client.DeleteInstance(ctx, projectId, instanceId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) - return - } - tflog.Info(ctx, "SQLServer Flex instance deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id -func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - - if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing instance", - fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - tflog.Info(ctx, "SQLServer Flex instance state imported") -} - -func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, model *Model, flavor *flavorModel, storage *storageModel, options *optionsModel, region string) error { - if resp == nil { - return fmt.Errorf("response input is nil") - } - if resp.Item == nil { - return fmt.Errorf("no instance provided") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - instance := resp.Item - - var instanceId string - if model.InstanceId.ValueString() != "" { - instanceId = model.InstanceId.ValueString() - } else if instance.Id != nil { - instanceId = *instance.Id - } else { - return fmt.Errorf("instance id not present") - } - - var aclList basetypes.ListValue - var diags diag.Diagnostics - if instance.Acl == nil || instance.Acl.Items == nil { - aclList = types.ListNull(types.StringType) - } else { - respACL := *instance.Acl.Items - modelACL, err := utils.ListValuetoStringSlice(model.ACL) - if err != nil { - return err - } - - reconciledACL := utils.ReconcileStringSlices(modelACL, respACL) - - aclList, diags = types.ListValueFrom(ctx, types.StringType, reconciledACL) - if diags.HasError() { - return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) - } - } - - var flavorValues map[string]attr.Value - if instance.Flavor == nil { - flavorValues = map[string]attr.Value{ - "id": flavor.Id, - "description": flavor.Description, - "cpu": flavor.CPU, - "ram": flavor.RAM, - } - } else { - flavorValues = map[string]attr.Value{ - "id": types.StringValue(*instance.Flavor.Id), - "description": types.StringValue(*instance.Flavor.Description), - "cpu": types.Int64PointerValue(instance.Flavor.Cpu), - "ram": types.Int64PointerValue(instance.Flavor.Memory), - } - } - flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues) - if diags.HasError() { - return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags)) - } - - var storageValues map[string]attr.Value - if instance.Storage == nil { - storageValues = map[string]attr.Value{ - "class": storage.Class, - "size": storage.Size, - } - } else { - storageValues = map[string]attr.Value{ - "class": types.StringValue(*instance.Storage.Class), - "size": types.Int64PointerValue(instance.Storage.Size), - } - } - storageObject, diags := types.ObjectValue(storageTypes, storageValues) - if diags.HasError() { - return fmt.Errorf("creating storage: %w", core.DiagsToError(diags)) - } - - var optionsValues map[string]attr.Value - if instance.Options == nil { - optionsValues = map[string]attr.Value{ - "edition": options.Edition, - "retention_days": options.RetentionDays, - } - } else { - retentionDays := options.RetentionDays - retentionDaysString, ok := (*instance.Options)["retentionDays"] - if ok { - retentionDaysValue, err := strconv.ParseInt(retentionDaysString, 10, 64) - if err != nil { - return fmt.Errorf("parse retentionDays to int64: %w", err) - } - retentionDays = types.Int64Value(retentionDaysValue) - } - - edition := options.Edition - editionValue, ok := (*instance.Options)["edition"] - if ok { - edition = types.StringValue(editionValue) - } - - optionsValues = map[string]attr.Value{ - "edition": edition, - "retention_days": retentionDays, - } - } - optionsObject, diags := types.ObjectValue(optionsTypes, optionsValues) - if diags.HasError() { - return fmt.Errorf("creating options: %w", core.DiagsToError(diags)) - } - - simplifiedModelBackupSchedule := utils.SimplifyBackupSchedule(model.BackupSchedule.ValueString()) - // If the value returned by the API is different from the one in the model after simplification, - // we update the model so that it causes an error in Terraform - if simplifiedModelBackupSchedule != types.StringPointerValue(instance.BackupSchedule).ValueString() { - model.BackupSchedule = types.StringPointerValue(instance.BackupSchedule) - } - - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, instanceId) - model.InstanceId = types.StringValue(instanceId) - model.Name = types.StringPointerValue(instance.Name) - model.ACL = aclList - model.Flavor = flavorObject - model.Replicas = types.Int64PointerValue(instance.Replicas) - model.Storage = storageObject - model.Version = types.StringPointerValue(instance.Version) - model.Options = optionsObject - model.Region = types.StringValue(region) - return nil -} - -func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *storageModel, options *optionsModel) (*sqlserverflex.CreateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - aclPayload := &sqlserverflex.CreateInstancePayloadAcl{} - if acl != nil { - aclPayload.Items = &acl - } - if flavor == nil { - return nil, fmt.Errorf("nil flavor") - } - storagePayload := &sqlserverflex.CreateInstancePayloadStorage{} - if storage != nil { - storagePayload.Class = conversion.StringValueToPointer(storage.Class) - storagePayload.Size = conversion.Int64ValueToPointer(storage.Size) - } - optionsPayload := &sqlserverflex.CreateInstancePayloadOptions{} - if options != nil { - optionsPayload.Edition = conversion.StringValueToPointer(options.Edition) - retentionDaysInt := conversion.Int64ValueToPointer(options.RetentionDays) - var retentionDays *string - if retentionDaysInt != nil { - retentionDays = coreUtils.Ptr(strconv.FormatInt(*retentionDaysInt, 10)) - } - optionsPayload.RetentionDays = retentionDays - } - - return &sqlserverflex.CreateInstancePayload{ - Acl: aclPayload, - BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule), - FlavorId: conversion.StringValueToPointer(flavor.Id), - Name: conversion.StringValueToPointer(model.Name), - Storage: storagePayload, - Version: conversion.StringValueToPointer(model.Version), - Options: optionsPayload, - }, nil -} - -func toUpdatePayload(model *Model, acl []string, flavor *flavorModel) (*sqlserverflex.PartialUpdateInstancePayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - aclPayload := &sqlserverflex.CreateInstancePayloadAcl{} - if acl != nil { - aclPayload.Items = &acl - } - if flavor == nil { - return nil, fmt.Errorf("nil flavor") - } - - return &sqlserverflex.PartialUpdateInstancePayload{ - Acl: aclPayload, - BackupSchedule: conversion.StringValueToPointer(model.BackupSchedule), - FlavorId: conversion.StringValueToPointer(flavor.Id), - Name: conversion.StringValueToPointer(model.Name), - Version: conversion.StringValueToPointer(model.Version), - }, nil -} - -type sqlserverflexClient interface { - ListFlavorsExecute(ctx context.Context, projectId, region string) (*sqlserverflex.ListFlavorsResponse, error) -} - -func loadFlavorId(ctx context.Context, client sqlserverflexClient, model *Model, flavor *flavorModel) error { - if model == nil { - return fmt.Errorf("nil model") - } - if flavor == nil { - return fmt.Errorf("nil flavor") - } - cpu := conversion.Int64ValueToPointer(flavor.CPU) - if cpu == nil { - return fmt.Errorf("nil CPU") - } - ram := conversion.Int64ValueToPointer(flavor.RAM) - if ram == nil { - return fmt.Errorf("nil RAM") - } - - projectId := model.ProjectId.ValueString() - region := model.Region.ValueString() - res, err := client.ListFlavorsExecute(ctx, projectId, region) - if err != nil { - return fmt.Errorf("listing sqlserverflex flavors: %w", err) - } - - avl := "" - if res.Flavors == nil { - return fmt.Errorf("finding flavors for project %s", projectId) - } - for _, f := range *res.Flavors { - if f.Id == nil || f.Cpu == nil || f.Memory == nil { - continue - } - if *f.Cpu == *cpu && *f.Memory == *ram { - flavor.Id = types.StringValue(*f.Id) - flavor.Description = types.StringValue(*f.Description) - break - } - avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory) - } - if flavor.Id.ValueString() == "" { - return fmt.Errorf("couldn't find flavor, available specs are:%s", avl) - } - - return nil -} diff --git a/stackit/internal/services/sqlserverflex/instance/resource_test.go b/stackit/internal/services/sqlserverflex/instance/resource_test.go deleted file mode 100644 index 66021845..00000000 --- a/stackit/internal/services/sqlserverflex/instance/resource_test.go +++ /dev/null @@ -1,821 +0,0 @@ -package sqlserverflex - -import ( - "context" - "fmt" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" -) - -type sqlserverflexClientMocked struct { - returnError bool - listFlavorsResp *sqlserverflex.ListFlavorsResponse -} - -func (c *sqlserverflexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*sqlserverflex.ListFlavorsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("get flavors failed") - } - - return c.listFlavorsResp, nil -} - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - state Model - input *sqlserverflex.GetInstanceResponse - flavor *flavorModel - storage *storageModel - options *optionsModel - region string - expected Model - isValid bool - }{ - { - "default_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &sqlserverflex.GetInstanceResponse{ - Item: &sqlserverflex.Instance{}, - }, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringNull(), - ACL: types.ListNull(types.StringType), - BackupSchedule: types.StringNull(), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Null(), - "ram": types.Int64Null(), - }), - Replicas: types.Int64Null(), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringNull(), - "size": types.Int64Null(), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "edition": types.StringNull(), - "retention_days": types.Int64Null(), - }), - Version: types.StringNull(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &sqlserverflex.GetInstanceResponse{ - Item: &sqlserverflex.Instance{ - Acl: &sqlserverflex.ACL{ - Items: &[]string{ - "ip1", - "ip2", - "", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: &sqlserverflex.Flavor{ - Cpu: utils.Ptr(int64(12)), - Description: utils.Ptr("description"), - Id: utils.Ptr("flavor_id"), - Memory: utils.Ptr(int64(34)), - }, - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: utils.Ptr("status"), - Storage: &sqlserverflex.Storage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(78)), - }, - Options: &map[string]string{ - "edition": "edition", - "retentionDays": "1", - }, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip1"), - types.StringValue("ip2"), - types.StringValue(""), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringValue("flavor_id"), - "description": types.StringValue("description"), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "edition": types.StringValue("edition"), - "retention_days": types.Int64Value(1), - }), - Version: types.StringValue("version"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values_no_flavor_and_storage", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &sqlserverflex.GetInstanceResponse{ - Item: &sqlserverflex.Instance{ - Acl: &sqlserverflex.ACL{ - Items: &[]string{ - "ip1", - "ip2", - "", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: nil, - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: utils.Ptr("status"), - Storage: nil, - Options: &map[string]string{ - "edition": "edition", - "retentionDays": "1", - }, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{ - CPU: types.Int64Value(12), - RAM: types.Int64Value(34), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(78), - }, - &optionsModel{ - Edition: types.StringValue("edition"), - RetentionDays: types.Int64Value(1), - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip1"), - types.StringValue("ip2"), - types.StringValue(""), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "edition": types.StringValue("edition"), - "retention_days": types.Int64Value(1), - }), - Version: types.StringValue("version"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "acls_unordered", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip2"), - types.StringValue(""), - types.StringValue("ip1"), - }), - }, - &sqlserverflex.GetInstanceResponse{ - Item: &sqlserverflex.Instance{ - Acl: &sqlserverflex.ACL{ - Items: &[]string{ - "", - "ip1", - "ip2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - Flavor: nil, - Id: utils.Ptr("iid"), - Name: utils.Ptr("name"), - Replicas: utils.Ptr(int64(56)), - Status: utils.Ptr("status"), - Storage: nil, - Options: &map[string]string{ - "edition": "edition", - "retentionDays": "1", - }, - Version: utils.Ptr("version"), - }, - }, - &flavorModel{ - CPU: types.Int64Value(12), - RAM: types.Int64Value(34), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(78), - }, - &optionsModel{}, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Name: types.StringValue("name"), - ACL: types.ListValueMust(types.StringType, []attr.Value{ - types.StringValue("ip2"), - types.StringValue(""), - types.StringValue("ip1"), - }), - BackupSchedule: types.StringValue("schedule"), - Flavor: types.ObjectValueMust(flavorTypes, map[string]attr.Value{ - "id": types.StringNull(), - "description": types.StringNull(), - "cpu": types.Int64Value(12), - "ram": types.Int64Value(34), - }), - Replicas: types.Int64Value(56), - Storage: types.ObjectValueMust(storageTypes, map[string]attr.Value{ - "class": types.StringValue("class"), - "size": types.Int64Value(78), - }), - Options: types.ObjectValueMust(optionsTypes, map[string]attr.Value{ - "edition": types.StringValue("edition"), - "retention_days": types.Int64Value(1), - }), - Version: types.StringValue("version"), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - nil, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - Model{ - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - }, - &sqlserverflex.GetInstanceResponse{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := mapFields(context.Background(), tt.input, &tt.state, tt.flavor, tt.storage, tt.options, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(tt.state, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputAcl []string - inputFlavor *flavorModel - inputStorage *storageModel - inputOptions *optionsModel - expected *sqlserverflex.CreateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - &sqlserverflex.CreateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{}, - }, - Storage: &sqlserverflex.CreateInstancePayloadStorage{}, - Options: &sqlserverflex.CreateInstancePayloadOptions{}, - }, - true, - }, - { - "simple_values", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Name: types.StringValue("name"), - Replicas: types.Int64Value(12), - Version: types.StringValue("version"), - }, - []string{ - "ip_1", - "ip_2", - }, - &flavorModel{ - Id: types.StringValue("flavor_id"), - }, - &storageModel{ - Class: types.StringValue("class"), - Size: types.Int64Value(34), - }, - &optionsModel{ - Edition: types.StringValue("edition"), - RetentionDays: types.Int64Value(1), - }, - &sqlserverflex.CreateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{ - "ip_1", - "ip_2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - FlavorId: utils.Ptr("flavor_id"), - Name: utils.Ptr("name"), - Storage: &sqlserverflex.CreateInstancePayloadStorage{ - Class: utils.Ptr("class"), - Size: utils.Ptr(int64(34)), - }, - Options: &sqlserverflex.CreateInstancePayloadOptions{ - Edition: utils.Ptr("edition"), - RetentionDays: utils.Ptr("1"), - }, - Version: utils.Ptr("version"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - BackupSchedule: types.StringNull(), - Name: types.StringNull(), - Replicas: types.Int64Value(2123456789), - Version: types.StringNull(), - }, - []string{ - "", - }, - &flavorModel{ - Id: types.StringNull(), - }, - &storageModel{ - Class: types.StringNull(), - Size: types.Int64Null(), - }, - &optionsModel{ - Edition: types.StringNull(), - RetentionDays: types.Int64Null(), - }, - &sqlserverflex.CreateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{ - "", - }, - }, - BackupSchedule: nil, - FlavorId: nil, - Name: nil, - Storage: &sqlserverflex.CreateInstancePayloadStorage{ - Class: nil, - Size: nil, - }, - Options: &sqlserverflex.CreateInstancePayloadOptions{}, - Version: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_acl", - &Model{}, - nil, - &flavorModel{}, - &storageModel{}, - &optionsModel{}, - &sqlserverflex.CreateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{}, - Storage: &sqlserverflex.CreateInstancePayloadStorage{}, - Options: &sqlserverflex.CreateInstancePayloadOptions{}, - }, - true, - }, - { - "nil_flavor", - &Model{}, - []string{}, - nil, - &storageModel{}, - &optionsModel{}, - nil, - false, - }, - { - "nil_storage", - &Model{}, - []string{}, - &flavorModel{}, - nil, - &optionsModel{}, - &sqlserverflex.CreateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{}, - }, - Storage: &sqlserverflex.CreateInstancePayloadStorage{}, - Options: &sqlserverflex.CreateInstancePayloadOptions{}, - }, - true, - }, - { - "nil_options", - &Model{}, - []string{}, - &flavorModel{}, - &storageModel{}, - nil, - &sqlserverflex.CreateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{}, - }, - Storage: &sqlserverflex.CreateInstancePayloadStorage{}, - Options: &sqlserverflex.CreateInstancePayloadOptions{}, - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputAcl, tt.inputFlavor, tt.inputStorage, tt.inputOptions) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToUpdatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputAcl []string - inputFlavor *flavorModel - expected *sqlserverflex.PartialUpdateInstancePayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &flavorModel{}, - &sqlserverflex.PartialUpdateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{}, - }, - }, - true, - }, - { - "simple_values", - &Model{ - BackupSchedule: types.StringValue("schedule"), - Name: types.StringValue("name"), - Replicas: types.Int64Value(12), - Version: types.StringValue("version"), - }, - []string{ - "ip_1", - "ip_2", - }, - &flavorModel{ - Id: types.StringValue("flavor_id"), - }, - &sqlserverflex.PartialUpdateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{ - "ip_1", - "ip_2", - }, - }, - BackupSchedule: utils.Ptr("schedule"), - FlavorId: utils.Ptr("flavor_id"), - Name: utils.Ptr("name"), - Version: utils.Ptr("version"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - BackupSchedule: types.StringNull(), - Name: types.StringNull(), - Replicas: types.Int64Value(2123456789), - Version: types.StringNull(), - }, - []string{ - "", - }, - &flavorModel{ - Id: types.StringNull(), - }, - &sqlserverflex.PartialUpdateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{ - Items: &[]string{ - "", - }, - }, - BackupSchedule: nil, - FlavorId: nil, - Name: nil, - Version: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - &flavorModel{}, - nil, - false, - }, - { - "nil_acl", - &Model{}, - nil, - &flavorModel{}, - &sqlserverflex.PartialUpdateInstancePayload{ - Acl: &sqlserverflex.CreateInstancePayloadAcl{}, - }, - true, - }, - { - "nil_flavor", - &Model{}, - []string{}, - nil, - nil, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toUpdatePayload(tt.input, tt.inputAcl, tt.inputFlavor) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestLoadFlavorId(t *testing.T) { - tests := []struct { - description string - inputFlavor *flavorModel - mockedResp *sqlserverflex.ListFlavorsResponse - expected *flavorModel - getFlavorsFails bool - isValid bool - }{ - { - "ok_flavor", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(2)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - }, - }, - &flavorModel{ - Id: types.StringValue("fid-1"), - Description: types.StringValue("description"), - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - true, - }, - { - "ok_flavor_2", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(2)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - { - Id: utils.Ptr("fid-2"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - &flavorModel{ - Id: types.StringValue("fid-1"), - Description: types.StringValue("description"), - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - true, - }, - { - "no_matching_flavor", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ - { - Id: utils.Ptr("fid-1"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(8)), - }, - { - Id: utils.Ptr("fid-2"), - Cpu: utils.Ptr(int64(1)), - Description: utils.Ptr("description"), - Memory: utils.Ptr(int64(4)), - }, - }, - }, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - false, - }, - { - "nil_response", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &sqlserverflex.ListFlavorsResponse{}, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - false, - false, - }, - { - "error_response", - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - &sqlserverflex.ListFlavorsResponse{}, - &flavorModel{ - CPU: types.Int64Value(2), - RAM: types.Int64Value(8), - }, - true, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - client := &sqlserverflexClientMocked{ - returnError: tt.getFlavorsFails, - listFlavorsResp: tt.mockedResp, - } - model := &Model{ - ProjectId: types.StringValue("pid"), - } - flavorModel := &flavorModel{ - CPU: tt.inputFlavor.CPU, - RAM: tt.inputFlavor.RAM, - } - err := loadFlavorId(context.Background(), client, model, flavorModel) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(flavorModel, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go b/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go deleted file mode 100644 index e88ac599..00000000 --- a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go +++ /dev/null @@ -1,480 +0,0 @@ -package sqlserverflex_test - -import ( - "context" - _ "embed" - "fmt" - "maps" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" - core_config "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" -) - -var ( - //go:embed testdata/resource-max.tf - resourceMaxConfig string - //go:embed testdata/resource-min.tf - resourceMinConfig string -) -var testConfigVarsMin = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "flavor_cpu": config.IntegerVariable(4), - "flavor_ram": config.IntegerVariable(16), - "flavor_description": config.StringVariable("SQLServer-Flex-4.16-Standard-EU01"), - "replicas": config.IntegerVariable(1), - "flavor_id": config.StringVariable("4.16-Single"), - "username": config.StringVariable(fmt.Sprintf("tf-acc-user-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlpha))), - "role": config.StringVariable("##STACKIT_LoginManager##"), -} - -var testConfigVarsMax = config.Variables{ - "project_id": config.StringVariable(testutil.ProjectId), - "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), - "acl1": config.StringVariable("192.168.0.0/16"), - "flavor_cpu": config.IntegerVariable(4), - "flavor_ram": config.IntegerVariable(16), - "flavor_description": config.StringVariable("SQLServer-Flex-4.16-Standard-EU01"), - "storage_class": config.StringVariable("premium-perf2-stackit"), - "storage_size": config.IntegerVariable(40), - "server_version": config.StringVariable("2022"), - "replicas": config.IntegerVariable(1), - "options_retention_days": config.IntegerVariable(64), - "flavor_id": config.StringVariable("4.16-Single"), - "backup_schedule": config.StringVariable("00 6 * * *"), - "username": config.StringVariable(fmt.Sprintf("tf-acc-user-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlpha))), - "role": config.StringVariable("##STACKIT_LoginManager##"), - "region": config.StringVariable(testutil.Region), -} - -func configVarsMinUpdated() config.Variables { - temp := maps.Clone(testConfigVarsMax) - temp["name"] = config.StringVariable(testutil.ConvertConfigVariable(temp["name"]) + "changed") - return temp -} - -func configVarsMaxUpdated() config.Variables { - temp := maps.Clone(testConfigVarsMax) - temp["backup_schedule"] = config.StringVariable("00 12 * * *") - return temp -} - -func TestAccSQLServerFlexMinResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccChecksqlserverflexDestroy, - Steps: []resource.TestStep{ - // Creation - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.description", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_description"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "replicas", testutil.ConvertConfigVariable(testConfigVarsMin["replicas"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_cpu"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_ram"])), - // User - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "project_id", - "stackit_sqlserverflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "instance_id", - "stackit_sqlserverflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "user_id"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "password"), - ), - }, - // Update - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.description", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_description"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_cpu"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_ram"])), - // User - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "project_id", - "stackit_sqlserverflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "instance_id", - "stackit_sqlserverflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "user_id"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "password"), - ), - }, - // data source - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: testConfigVarsMin, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMin["name"])), - resource.TestCheckResourceAttrPair( - "data.stackit_sqlserverflex_instance.instance", "project_id", - "stackit_sqlserverflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_sqlserverflex_instance.instance", "instance_id", - "stackit_sqlserverflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_sqlserverflex_user.user", "instance_id", - "stackit_sqlserverflex_user.user", "instance_id", - ), - - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.id", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_id"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.description", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_description"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_cpu"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(testConfigVarsMin["flavor_ram"])), - - // User data - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_user.user", "user_id"), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "username", testutil.ConvertConfigVariable(testConfigVarsMin["username"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "roles.#", "1"), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "roles.0", testutil.ConvertConfigVariable(testConfigVarsMax["role"])), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMin, - ResourceName: "stackit_sqlserverflex_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_sqlserverflex_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_sqlserverflex_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"backup_schedule"}, - ImportStateCheck: func(s []*terraform.InstanceState) error { - if len(s) != 1 { - return fmt.Errorf("expected 1 state, got %d", len(s)) - } - return nil - }, - }, - { - ResourceName: "stackit_sqlserverflex_user.user", - ConfigVariables: testConfigVarsMin, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_sqlserverflex_user.user"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_sqlserverflex_user.user") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - userId, ok := r.Primary.Attributes["user_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute user_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId, userId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password"}, - }, - // Update - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMinConfig, - ConfigVariables: configVarsMinUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(configVarsMinUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(configVarsMinUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.description"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(configVarsMinUpdated()["flavor_cpu"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(configVarsMinUpdated()["flavor_ram"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func TestAccSQLServerFlexMaxResource(t *testing.T) { - resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccChecksqlserverflexDestroy, - Steps: []resource.TestStep{ - // Creation - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl1"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.description", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_description"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "replicas", testutil.ConvertConfigVariable(testConfigVarsMax["replicas"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_cpu"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_ram"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "storage.class", testutil.ConvertConfigVariable(testConfigVarsMax["storage_class"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "storage.size", testutil.ConvertConfigVariable(testConfigVarsMax["storage_size"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMax["server_version"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "options.retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["options_retention_days"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "backup_schedule", testutil.ConvertConfigVariable(testConfigVarsMax["backup_schedule"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "region", testutil.Region), - // User - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "project_id", - "stackit_sqlserverflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "instance_id", - "stackit_sqlserverflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "user_id"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "password"), - ), - }, - // Update - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl1"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.description", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_description"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "replicas", testutil.ConvertConfigVariable(testConfigVarsMax["replicas"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_cpu"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_ram"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "storage.class", testutil.ConvertConfigVariable(testConfigVarsMax["storage_class"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "storage.size", testutil.ConvertConfigVariable(testConfigVarsMax["storage_size"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "version", testutil.ConvertConfigVariable(testConfigVarsMax["server_version"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "options.retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["options_retention_days"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "backup_schedule", testutil.ConvertConfigVariable(testConfigVarsMax["backup_schedule"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "region", testutil.Region), - // User - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "project_id", - "stackit_sqlserverflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "stackit_sqlserverflex_user.user", "instance_id", - "stackit_sqlserverflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "user_id"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_user.user", "password"), - ), - }, - // data source - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: testConfigVarsMax, - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])), - resource.TestCheckResourceAttrPair( - "data.stackit_sqlserverflex_instance.instance", "project_id", - "stackit_sqlserverflex_instance.instance", "project_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_sqlserverflex_instance.instance", "instance_id", - "stackit_sqlserverflex_instance.instance", "instance_id", - ), - resource.TestCheckResourceAttrPair( - "data.stackit_sqlserverflex_user.user", "instance_id", - "stackit_sqlserverflex_user.user", "instance_id", - ), - - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "acl.0", testutil.ConvertConfigVariable(testConfigVarsMax["acl1"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.id", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_id"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.description", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_description"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_cpu"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(testConfigVarsMax["flavor_ram"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "replicas", testutil.ConvertConfigVariable(testConfigVarsMax["replicas"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "options.retention_days", testutil.ConvertConfigVariable(testConfigVarsMax["options_retention_days"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_instance.instance", "backup_schedule", testutil.ConvertConfigVariable(testConfigVarsMax["backup_schedule"])), - - // User data - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "project_id", testutil.ConvertConfigVariable(testConfigVarsMax["project_id"])), - resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_user.user", "user_id"), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "username", testutil.ConvertConfigVariable(testConfigVarsMax["username"])), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "roles.#", "1"), - resource.TestCheckResourceAttr("data.stackit_sqlserverflex_user.user", "roles.0", testutil.ConvertConfigVariable(testConfigVarsMax["role"])), - resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_user.user", "host"), - resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_user.user", "port"), - ), - }, - // Import - { - ConfigVariables: testConfigVarsMax, - ResourceName: "stackit_sqlserverflex_instance.instance", - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_sqlserverflex_instance.instance"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_sqlserverflex_instance.instance") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - - return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"backup_schedule"}, - ImportStateCheck: func(s []*terraform.InstanceState) error { - if len(s) != 1 { - return fmt.Errorf("expected 1 state, got %d", len(s)) - } - if s[0].Attributes["backup_schedule"] != testutil.ConvertConfigVariable(testConfigVarsMax["backup_schedule"]) { - return fmt.Errorf("expected backup_schedule %s, got %s", testConfigVarsMax["backup_schedule"], s[0].Attributes["backup_schedule"]) - } - return nil - }, - }, - { - ResourceName: "stackit_sqlserverflex_user.user", - ConfigVariables: testConfigVarsMax, - ImportStateIdFunc: func(s *terraform.State) (string, error) { - r, ok := s.RootModule().Resources["stackit_sqlserverflex_user.user"] - if !ok { - return "", fmt.Errorf("couldn't find resource stackit_sqlserverflex_user.user") - } - instanceId, ok := r.Primary.Attributes["instance_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute instance_id") - } - userId, ok := r.Primary.Attributes["user_id"] - if !ok { - return "", fmt.Errorf("couldn't find attribute user_id") - } - - return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, instanceId, userId), nil - }, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"password"}, - }, - // Update - { - Config: testutil.SQLServerFlexProviderConfig() + "\n" + resourceMaxConfig, - ConfigVariables: configVarsMaxUpdated(), - Check: resource.ComposeAggregateTestCheckFunc( - // Instance data - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "project_id", testutil.ConvertConfigVariable(configVarsMaxUpdated()["project_id"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "instance_id"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "name", testutil.ConvertConfigVariable(configVarsMaxUpdated()["name"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.#", "1"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "acl.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["acl1"])), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.id"), - resource.TestCheckResourceAttrSet("stackit_sqlserverflex_instance.instance", "flavor.description"), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.cpu", testutil.ConvertConfigVariable(configVarsMaxUpdated()["flavor_cpu"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "flavor.ram", testutil.ConvertConfigVariable(configVarsMaxUpdated()["flavor_ram"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "replicas", testutil.ConvertConfigVariable(configVarsMaxUpdated()["replicas"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "storage.class", testutil.ConvertConfigVariable(configVarsMaxUpdated()["storage_class"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "storage.size", testutil.ConvertConfigVariable(configVarsMaxUpdated()["storage_size"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "version", testutil.ConvertConfigVariable(configVarsMaxUpdated()["server_version"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "options.retention_days", testutil.ConvertConfigVariable(configVarsMaxUpdated()["options_retention_days"])), - resource.TestCheckResourceAttr("stackit_sqlserverflex_instance.instance", "backup_schedule", testutil.ConvertConfigVariable(configVarsMaxUpdated()["backup_schedule"])), - ), - }, - // Deletion is done by the framework implicitly - }, - }) -} - -func testAccChecksqlserverflexDestroy(s *terraform.State) error { - ctx := context.Background() - var client *sqlserverflex.APIClient - var err error - if testutil.SQLServerFlexCustomEndpoint == "" { - client, err = sqlserverflex.NewAPIClient() - } else { - client, err = sqlserverflex.NewAPIClient( - core_config.WithEndpoint(testutil.SQLServerFlexCustomEndpoint), - ) - } - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_sqlserverflex_instance" { - continue - } - // instance terraform ID: = "[project_id],[region],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[2] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId, testutil.Region).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - items := *instancesResp.Items - for i := range items { - if items[i].Id == nil { - continue - } - if utils.Contains(instancesToDestroy, *items[i].Id) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *items[i].Id, testutil.Region) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *items[i].Id, err) - } - _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *items[i].Id, testutil.Region).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *items[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/sqlserverflex/testdata/resource-max.tf b/stackit/internal/services/sqlserverflex/testdata/resource-max.tf deleted file mode 100644 index a0cf700a..00000000 --- a/stackit/internal/services/sqlserverflex/testdata/resource-max.tf +++ /dev/null @@ -1,51 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "acl1" {} -variable "flavor_cpu" {} -variable "flavor_ram" {} -variable "storage_class" {} -variable "storage_size" {} -variable "options_retention_days" {} -variable "backup_schedule" {} -variable "username" {} -variable "role" {} -variable "server_version" {} -variable "region" {} - -resource "stackit_sqlserverflex_instance" "instance" { - project_id = var.project_id - name = var.name - acl = [var.acl1] - flavor = { - cpu = var.flavor_cpu - ram = var.flavor_ram - } - storage = { - class = var.storage_class - size = var.storage_size - } - version = var.server_version - options = { - retention_days = var.options_retention_days - } - backup_schedule = var.backup_schedule - region = var.region -} - -resource "stackit_sqlserverflex_user" "user" { - project_id = stackit_sqlserverflex_instance.instance.project_id - instance_id = stackit_sqlserverflex_instance.instance.instance_id - username = var.username - roles = [var.role] -} - -data "stackit_sqlserverflex_instance" "instance" { - project_id = var.project_id - instance_id = stackit_sqlserverflex_instance.instance.instance_id -} - -data "stackit_sqlserverflex_user" "user" { - project_id = var.project_id - instance_id = stackit_sqlserverflex_instance.instance.instance_id - user_id = stackit_sqlserverflex_user.user.user_id -} diff --git a/stackit/internal/services/sqlserverflex/testdata/resource-min.tf b/stackit/internal/services/sqlserverflex/testdata/resource-min.tf deleted file mode 100644 index 3953ddf1..00000000 --- a/stackit/internal/services/sqlserverflex/testdata/resource-min.tf +++ /dev/null @@ -1,33 +0,0 @@ -variable "project_id" {} -variable "name" {} -variable "flavor_cpu" {} -variable "flavor_ram" {} -variable "username" {} -variable "role" {} - -resource "stackit_sqlserverflex_instance" "instance" { - project_id = var.project_id - name = var.name - flavor = { - cpu = var.flavor_cpu - ram = var.flavor_ram - } -} - -resource "stackit_sqlserverflex_user" "user" { - project_id = stackit_sqlserverflex_instance.instance.project_id - instance_id = stackit_sqlserverflex_instance.instance.instance_id - username = var.username - roles = [var.role] -} - -data "stackit_sqlserverflex_instance" "instance" { - project_id = var.project_id - instance_id = stackit_sqlserverflex_instance.instance.instance_id -} - -data "stackit_sqlserverflex_user" "user" { - project_id = var.project_id - instance_id = stackit_sqlserverflex_instance.instance.instance_id - user_id = stackit_sqlserverflex_user.user.user_id -} diff --git a/stackit/internal/services/sqlserverflex/user/datasource.go b/stackit/internal/services/sqlserverflex/user/datasource.go deleted file mode 100644 index cb0980f8..00000000 --- a/stackit/internal/services/sqlserverflex/user/datasource.go +++ /dev/null @@ -1,235 +0,0 @@ -package sqlserverflex - -import ( - "context" - "fmt" - "net/http" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ datasource.DataSource = &userDataSource{} -) - -type DataSourceModel struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` -} - -// NewUserDataSource is a helper function to simplify the provider implementation. -func NewUserDataSource() datasource.DataSource { - return &userDataSource{} -} - -// userDataSource is the data source implementation. -type userDataSource struct { - client *sqlserverflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the data source type name. -func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_sqlserverflex_user" -} - -// Configure adds the provider configured client to the data source. -func (r *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "SQLServer Flex user client configured") -} - -// Schema defines the schema for the data source. -func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SQLServer Flex user data source schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal data source. ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the SQLServer Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "username": "Username of the SQLServer Flex instance.", - "roles": "Database access levels for the user.", - "password": "Password of the user account.", - "region": "The resource region. If not defined, the provider region is used.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Required: true, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Computed: true, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Computed: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - // the region cannot be found automatically, so it has to be passed - Optional: true, - Description: descriptions["region"], - }, - }, - } -} - -// Read refreshes the Terraform state with the latest data. -func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model DataSourceModel - diags := req.Config.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - ctx = tflog.SetField(ctx, "region", region) - - recordSetResp, err := r.client.GetUser(ctx, projectId, instanceId, userId, region).Execute() - if err != nil { - utils.LogError( - ctx, - &resp.Diagnostics, - err, - "Reading user", - fmt.Sprintf("User with ID %q or instance with ID %q does not exist in project %q.", userId, instanceId, projectId), - map[int]string{ - http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId), - }, - ) - resp.State.RemoveResource(ctx) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema and populate Computed attribute values - err = mapDataSourceFields(recordSetResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SQLServer Flex user read") -} - -func mapDataSourceFields(userResp *sqlserverflex.GetUserResponse, model *DataSourceModel, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, - ) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - - if user.Roles == nil { - model.Roles = types.SetNull(types.StringType) - } else { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - model.Region = types.StringValue(region) - return nil -} diff --git a/stackit/internal/services/sqlserverflex/user/datasource_test.go b/stackit/internal/services/sqlserverflex/user/datasource_test.go deleted file mode 100644 index b5179c44..00000000 --- a/stackit/internal/services/sqlserverflex/user/datasource_test.go +++ /dev/null @@ -1,144 +0,0 @@ -package sqlserverflex - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" -) - -func TestMapDataSourceFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *sqlserverflex.GetUserResponse - region string - expected DataSourceModel - isValid bool - }{ - { - "default_values", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{}, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{ - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{ - Id: utils.Ptr("uid"), - Roles: &[]string{}, - Username: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - }, - testRegion, - DataSourceModel{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - DataSourceModel{}, - false, - }, - { - "nil_response_2", - &sqlserverflex.GetUserResponse{}, - testRegion, - DataSourceModel{}, - false, - }, - { - "no_resource_id", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{}, - }, - testRegion, - DataSourceModel{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &DataSourceModel{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapDataSourceFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/sqlserverflex/user/resource.go b/stackit/internal/services/sqlserverflex/user/resource.go deleted file mode 100644 index e73fb9b0..00000000 --- a/stackit/internal/services/sqlserverflex/user/resource.go +++ /dev/null @@ -1,487 +0,0 @@ -package sqlserverflex - -import ( - "context" - "fmt" - "net/http" - "strings" - - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/resource" - "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" -) - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &userResource{} - _ resource.ResourceWithConfigure = &userResource{} - _ resource.ResourceWithImportState = &userResource{} - _ resource.ResourceWithModifyPlan = &userResource{} -) - -type Model struct { - Id types.String `tfsdk:"id"` // needed by TF - UserId types.String `tfsdk:"user_id"` - InstanceId types.String `tfsdk:"instance_id"` - ProjectId types.String `tfsdk:"project_id"` - Username types.String `tfsdk:"username"` - Roles types.Set `tfsdk:"roles"` - Password types.String `tfsdk:"password"` - Host types.String `tfsdk:"host"` - Port types.Int64 `tfsdk:"port"` - Region types.String `tfsdk:"region"` -} - -// NewUserResource is a helper function to simplify the provider implementation. -func NewUserResource() resource.Resource { - return &userResource{} -} - -// userResource is the resource implementation. -type userResource struct { - client *sqlserverflex.APIClient - providerData core.ProviderData -} - -// Metadata returns the resource type name. -func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_sqlserverflex_user" -} - -// Configure adds the provider configured client to the resource. -func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { - var ok bool - r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) - if !ok { - return - } - - apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) - if resp.Diagnostics.HasError() { - return - } - r.client = apiClient - tflog.Info(ctx, "SQLServer Flex user client configured") -} - -// ModifyPlan implements resource.ResourceWithModifyPlan. -// Use the modifier to set the effective region in the current plan. -func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform - var configModel Model - // skip initial empty configuration to avoid follow-up errors - if req.Config.Raw.IsNull() { - return - } - resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) - if resp.Diagnostics.HasError() { - return - } - - var planModel Model - resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) - if resp.Diagnostics.HasError() { - return - } - - utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) - if resp.Diagnostics.HasError() { - return - } - - resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) - if resp.Diagnostics.HasError() { - return - } -} - -// Schema defines the schema for the resource. -func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { - descriptions := map[string]string{ - "main": "SQLServer Flex user resource schema. Must have a `region` specified in the provider configuration.", - "id": "Terraform's internal resource ID. It is structured as \"`project_id`,`region`,`instance_id`,`user_id`\".", - "user_id": "User ID.", - "instance_id": "ID of the SQLServer Flex instance.", - "project_id": "STACKIT project ID to which the instance is associated.", - "username": "Username of the SQLServer Flex instance.", - "roles": "Database access levels for the user. The values for the default roles are: `##STACKIT_DatabaseManager##`, `##STACKIT_LoginManager##`, `##STACKIT_ProcessManager##`, `##STACKIT_ServerManager##`, `##STACKIT_SQLAgentManager##`, `##STACKIT_SQLAgentUser##`", - "password": "Password of the user account.", - } - - resp.Schema = schema.Schema{ - Description: descriptions["main"], - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Description: descriptions["id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "user_id": schema.StringAttribute{ - Description: descriptions["user_id"], - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.NoSeparator(), - }, - }, - "instance_id": schema.StringAttribute{ - Description: descriptions["instance_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "project_id": schema.StringAttribute{ - Description: descriptions["project_id"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - Validators: []validator.String{ - validate.UUID(), - validate.NoSeparator(), - }, - }, - "username": schema.StringAttribute{ - Description: descriptions["username"], - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - "roles": schema.SetAttribute{ - Description: descriptions["roles"], - ElementType: types.StringType, - Required: true, - PlanModifiers: []planmodifier.Set{ - setplanmodifier.RequiresReplace(), - }, - }, - "password": schema.StringAttribute{ - Description: descriptions["password"], - Computed: true, - Sensitive: true, - }, - "host": schema.StringAttribute{ - Computed: true, - }, - "port": schema.Int64Attribute{ - Computed: true, - }, - "region": schema.StringAttribute{ - Optional: true, - // must be computed to allow for storing the override value from the provider - Computed: true, - Description: descriptions["region"], - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - }, - } -} - -// Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.Plan.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - region := model.Region.ValueString() - - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "region", region) - - var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { - diags = model.Roles.ElementsAs(ctx, &roles, false) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - } - - // Generate API request body from model - payload, err := toCreatePayload(&model, roles) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Creating API payload: %v", err)) - return - } - // Create new user - userResp, err := r.client.CreateUser(ctx, projectId, instanceId, region).CreateUserPayload(*payload).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user Id. A user might have been created") - return - } - userId := *userResp.Item.Id - ctx = tflog.SetField(ctx, "user_id", userId) - - // Map response body to schema - err = mapFieldsCreate(userResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - // Set state to fully populated data - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SQLServer Flex user created") -} - -// Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - region := r.providerData.GetRegionWithOverride(model.Region) - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - ctx = tflog.SetField(ctx, "region", region) - - recordSetResp, err := r.client.GetUser(ctx, projectId, instanceId, userId, region).Execute() - if err != nil { - oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if ok && oapiErr.StatusCode == http.StatusNotFound { - resp.State.RemoveResource(ctx) - return - } - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - // Map response body to schema - err = mapFields(recordSetResp, &model, region) - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Processing API payload: %v", err)) - return - } - - // Set refreshed state - diags = resp.State.Set(ctx, model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - tflog.Info(ctx, "SQLServer Flex user read") -} - -// Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform - // Update shouldn't be called - core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated") -} - -// Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform - // Retrieve values from plan - var model Model - diags := req.State.Get(ctx, &model) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - ctx = core.InitProviderContext(ctx) - - projectId := model.ProjectId.ValueString() - instanceId := model.InstanceId.ValueString() - userId := model.UserId.ValueString() - region := model.Region.ValueString() - ctx = tflog.SetField(ctx, "project_id", projectId) - ctx = tflog.SetField(ctx, "instance_id", instanceId) - ctx = tflog.SetField(ctx, "user_id", userId) - ctx = tflog.SetField(ctx, "region", region) - - // Delete existing record set - err := r.client.DeleteUser(ctx, projectId, instanceId, userId, region).Execute() - if err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) - return - } - - ctx = core.LogResponse(ctx) - - tflog.Info(ctx, "SQLServer Flex user deleted") -} - -// ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id -func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, core.Separator) - if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { - core.LogAndAddError(ctx, &resp.Diagnostics, - "Error importing user", - fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", req.ID), - ) - return - } - - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), idParts[1])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("instance_id"), idParts[2])...) - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("user_id"), idParts[3])...) - core.LogAndAddWarning(ctx, &resp.Diagnostics, - "SQLServer Flex user imported with empty password", - "The user password is not imported as it is only available upon creation of a new user. The password field will be empty.", - ) - tflog.Info(ctx, "SQLServer Flex user state imported") -} - -func mapFieldsCreate(userResp *sqlserverflex.CreateUserResponse, model *Model, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - if user.Id == nil { - return fmt.Errorf("user id not present") - } - userId := *user.Id - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - - if user.Password == nil { - return fmt.Errorf("user password not present") - } - model.Password = types.StringValue(*user.Password) - - if user.Roles != nil { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - - if model.Roles.IsNull() || model.Roles.IsUnknown() { - model.Roles = types.SetNull(types.StringType) - } - - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - model.Region = types.StringValue(region) - return nil -} - -func mapFields(userResp *sqlserverflex.GetUserResponse, model *Model, region string) error { - if userResp == nil || userResp.Item == nil { - return fmt.Errorf("response is nil") - } - if model == nil { - return fmt.Errorf("model input is nil") - } - user := userResp.Item - - var userId string - if model.UserId.ValueString() != "" { - userId = model.UserId.ValueString() - } else if user.Id != nil { - userId = *user.Id - } else { - return fmt.Errorf("user id not present") - } - model.Id = utils.BuildInternalTerraformId( - model.ProjectId.ValueString(), - region, - model.InstanceId.ValueString(), - userId, - ) - model.UserId = types.StringValue(userId) - model.Username = types.StringPointerValue(user.Username) - - if user.Roles != nil { - roles := []attr.Value{} - for _, role := range *user.Roles { - roles = append(roles, types.StringValue(role)) - } - rolesSet, diags := types.SetValue(types.StringType, roles) - if diags.HasError() { - return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) - } - model.Roles = rolesSet - } - - if model.Roles.IsNull() || model.Roles.IsUnknown() { - model.Roles = types.SetNull(types.StringType) - } - - model.Host = types.StringPointerValue(user.Host) - model.Port = types.Int64PointerValue(user.Port) - model.Region = types.StringValue(region) - return nil -} - -func toCreatePayload(model *Model, roles []string) (*sqlserverflex.CreateUserPayload, error) { - if model == nil { - return nil, fmt.Errorf("nil model") - } - - return &sqlserverflex.CreateUserPayload{ - Username: conversion.StringValueToPointer(model.Username), - Roles: &roles, - }, nil -} diff --git a/stackit/internal/services/sqlserverflex/user/resource_test.go b/stackit/internal/services/sqlserverflex/user/resource_test.go deleted file mode 100644 index 058b213d..00000000 --- a/stackit/internal/services/sqlserverflex/user/resource_test.go +++ /dev/null @@ -1,387 +0,0 @@ -package sqlserverflex - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" -) - -func TestMapFieldsCreate(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *sqlserverflex.CreateUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &sqlserverflex.CreateUserResponse{ - Item: &sqlserverflex.SingleUser{ - Id: utils.Ptr("uid"), - Password: utils.Ptr(""), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Password: types.StringValue(""), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &sqlserverflex.CreateUserResponse{ - Item: &sqlserverflex.SingleUser{ - Id: utils.Ptr("uid"), - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Password: utils.Ptr("password"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Password: types.StringValue("password"), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &sqlserverflex.CreateUserResponse{ - Item: &sqlserverflex.SingleUser{ - Id: utils.Ptr("uid"), - Roles: &[]string{}, - Username: nil, - Password: utils.Ptr(""), - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Password: types.StringValue(""), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &sqlserverflex.CreateUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &sqlserverflex.CreateUserResponse{ - Item: &sqlserverflex.SingleUser{}, - }, - testRegion, - Model{}, - false, - }, - { - "no_password", - &sqlserverflex.CreateUserResponse{ - Item: &sqlserverflex.SingleUser{ - Id: utils.Ptr("uid"), - }, - }, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - } - err := mapFieldsCreate(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestMapFields(t *testing.T) { - const testRegion = "region" - tests := []struct { - description string - input *sqlserverflex.GetUserResponse - region string - expected Model - isValid bool - }{ - { - "default_values", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{}, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetNull(types.StringType), - Host: types.StringNull(), - Port: types.Int64Null(), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "simple_values", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{ - Roles: &[]string{ - "role_1", - "role_2", - "", - }, - Username: utils.Ptr("username"), - Host: utils.Ptr("host"), - Port: utils.Ptr(int64(1234)), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringValue("username"), - Roles: types.SetValueMust(types.StringType, []attr.Value{ - types.StringValue("role_1"), - types.StringValue("role_2"), - types.StringValue(""), - }), - Host: types.StringValue("host"), - Port: types.Int64Value(1234), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "null_fields_and_int_conversions", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{ - Id: utils.Ptr("uid"), - Roles: &[]string{}, - Username: nil, - Host: nil, - Port: utils.Ptr(int64(2123456789)), - }, - }, - testRegion, - Model{ - Id: types.StringValue("pid,region,iid,uid"), - UserId: types.StringValue("uid"), - InstanceId: types.StringValue("iid"), - ProjectId: types.StringValue("pid"), - Username: types.StringNull(), - Roles: types.SetValueMust(types.StringType, []attr.Value{}), - Host: types.StringNull(), - Port: types.Int64Value(2123456789), - Region: types.StringValue(testRegion), - }, - true, - }, - { - "nil_response", - nil, - testRegion, - Model{}, - false, - }, - { - "nil_response_2", - &sqlserverflex.GetUserResponse{}, - testRegion, - Model{}, - false, - }, - { - "no_resource_id", - &sqlserverflex.GetUserResponse{ - Item: &sqlserverflex.UserResponseUser{}, - }, - testRegion, - Model{}, - false, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - state := &Model{ - ProjectId: tt.expected.ProjectId, - InstanceId: tt.expected.InstanceId, - UserId: tt.expected.UserId, - } - err := mapFields(tt.input, state, tt.region) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(state, &tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} - -func TestToCreatePayload(t *testing.T) { - tests := []struct { - description string - input *Model - inputRoles []string - expected *sqlserverflex.CreateUserPayload - isValid bool - }{ - { - "default_values", - &Model{}, - []string{}, - &sqlserverflex.CreateUserPayload{ - Roles: &[]string{}, - Username: nil, - }, - true, - }, - { - "default_values", - &Model{ - Username: types.StringValue("username"), - }, - []string{ - "role_1", - "role_2", - }, - &sqlserverflex.CreateUserPayload{ - Roles: &[]string{ - "role_1", - "role_2", - }, - Username: utils.Ptr("username"), - }, - true, - }, - { - "null_fields_and_int_conversions", - &Model{ - Username: types.StringNull(), - }, - []string{ - "", - }, - &sqlserverflex.CreateUserPayload{ - Roles: &[]string{ - "", - }, - Username: nil, - }, - true, - }, - { - "nil_model", - nil, - []string{}, - nil, - false, - }, - { - "nil_roles", - &Model{ - Username: types.StringValue("username"), - }, - []string{}, - &sqlserverflex.CreateUserPayload{ - Roles: &[]string{}, - Username: utils.Ptr("username"), - }, - true, - }, - } - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - output, err := toCreatePayload(tt.input, tt.inputRoles) - if !tt.isValid && err == nil { - t.Fatalf("Should have failed") - } - if tt.isValid && err != nil { - t.Fatalf("Should not have failed: %v", err) - } - if tt.isValid { - diff := cmp.Diff(output, tt.expected) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } - } - }) - } -} diff --git a/stackit/internal/services/sqlserverflex/utils/util.go b/stackit/internal/services/sqlserverflex/utils/util.go deleted file mode 100644 index 5c14c085..00000000 --- a/stackit/internal/services/sqlserverflex/utils/util.go +++ /dev/null @@ -1,32 +0,0 @@ -package utils - -import ( - "context" - "fmt" - - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *sqlserverflex.APIClient { - apiClientConfigOptions := []config.ConfigurationOption{ - config.WithCustomAuth(providerData.RoundTripper), - utils.UserAgentConfigOption(providerData.Version), - } - if providerData.SQLServerFlexCustomEndpoint != "" { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.SQLServerFlexCustomEndpoint)) - } else { - apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) - } - apiClient, err := sqlserverflex.NewAPIClient(apiClientConfigOptions...) - if err != nil { - core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) - return nil - } - - return apiClient -} diff --git a/stackit/internal/services/sqlserverflex/utils/util_test.go b/stackit/internal/services/sqlserverflex/utils/util_test.go deleted file mode 100644 index 5ee93949..00000000 --- a/stackit/internal/services/sqlserverflex/utils/util_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package utils - -import ( - "context" - "os" - "reflect" - "testing" - - "github.com/hashicorp/terraform-plugin-framework/diag" - sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" - "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" -) - -const ( - testVersion = "1.2.3" - testCustomEndpoint = "https://sqlserverflex-custom-endpoint.api.stackit.cloud" -) - -func TestConfigureClient(t *testing.T) { - /* mock authentication by setting service account token env variable */ - os.Clearenv() - err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") - if err != nil { - t.Errorf("error setting env variable: %v", err) - } - - type args struct { - providerData *core.ProviderData - } - tests := []struct { - name string - args args - wantErr bool - expected *sqlserverflex.APIClient - }{ - { - name: "default endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - }, - }, - expected: func() *sqlserverflex.APIClient { - apiClient, err := sqlserverflex.NewAPIClient( - config.WithRegion("eu01"), - utils.UserAgentConfigOption(testVersion), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - { - name: "custom endpoint", - args: args{ - providerData: &core.ProviderData{ - Version: testVersion, - SQLServerFlexCustomEndpoint: testCustomEndpoint, - }, - }, - expected: func() *sqlserverflex.APIClient { - apiClient, err := sqlserverflex.NewAPIClient( - utils.UserAgentConfigOption(testVersion), - config.WithEndpoint(testCustomEndpoint), - ) - if err != nil { - t.Errorf("error configuring client: %v", err) - } - return apiClient - }(), - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := context.Background() - diags := diag.Diagnostics{} - - actual := ConfigureClient(ctx, tt.args.providerData, &diags) - if diags.HasError() != tt.wantErr { - t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) - } - - if !reflect.DeepEqual(actual, tt.expected) { - t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) - } - }) - } -} diff --git a/stackit/provider.go b/stackit/provider.go index ed100ffd..fa2ec888 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -19,90 +18,13 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/access_token" roleAssignements "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/roleassignments" - cdnCustomDomain "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/customdomain" - cdn "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/distribution" - dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset" - dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone" - gitInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/instance" - iaasAffinityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/affinitygroup" - iaasImage "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/image" - iaasImageV2 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/imagev2" - iaasKeyPair "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/keypair" - machineType "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/machinetype" - iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network" - iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea" - iaasNetworkAreaRegion "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearegion" - iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute" - iaasNetworkInterface "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterface" - iaasNetworkInterfaceAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterfaceattach" - iaasProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/project" - iaasPublicIp "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicip" - iaasPublicIpAssociate "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicipassociate" - iaasPublicIpRanges "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicipranges" - iaasSecurityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/securitygroup" - iaasSecurityGroupRule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/securitygrouprule" - iaasServer "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/server" - iaasServiceAccountAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/serviceaccountattach" - iaasVolume "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volume" - iaasVolumeAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volumeattach" - iaasalphaRoutingTableRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/route" - iaasalphaRoutingTableRoutes "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/routes" - iaasalphaRoutingTable "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/table" - iaasalphaRoutingTables "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/tables" - kmsKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/key" - kmsKeyRing "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/keyring" - kmsWrappingKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/wrapping-key" - loadBalancer "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/loadbalancer" - loadBalancerObservabilityCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/observability-credential" - logMeCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/credential" - logMeInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/instance" - mariaDBCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/credential" - mariaDBInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/instance" - modelServingToken "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/modelserving/token" - mongoDBFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/instance" - mongoDBFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/user" - objectStorageBucket "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/bucket" - objecStorageCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/credential" - objecStorageCredentialsGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/credentialsgroup" - alertGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/alertgroup" - observabilityCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/credential" - observabilityInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/instance" - logAlertGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/log-alertgroup" - observabilityScrapeConfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/scrapeconfig" - openSearchCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/credential" - openSearchInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/instance" - postgresFlexDatabase "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/database" - postgresFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/instance" - postgresFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/user" postgresFlexAlphaInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflexalpha/instance" - rabbitMQCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/credential" - rabbitMQInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/instance" - redisCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/credential" - redisInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/instance" - resourceManagerFolder "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/folder" - resourceManagerProject "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/project" - scfOrganization "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/organization" - scfOrganizationmanager "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/organizationmanager" - scfPlatform "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/platform" - secretsManagerInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/instance" - secretsManagerUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/user" - serverBackupSchedule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/schedule" - serverUpdateSchedule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/schedule" - serviceAccount "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/account" - serviceAccountKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/key" - serviceAccountToken "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/token" - skeCluster "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/cluster" - skeKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/kubeconfig" - sqlServerFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/instance" - sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" ) // Ensure the implementation satisfies the expected interfaces var ( - _ provider.Provider = &Provider{} - _ provider.ProviderWithEphemeralResources = &Provider{} + _ provider.Provider = &Provider{} ) // Provider is the provider implementation. @@ -491,158 +413,15 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // DataSources defines the data sources implemented in the provider. func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{ - alertGroup.NewAlertGroupDataSource, - cdn.NewDistributionDataSource, - cdnCustomDomain.NewCustomDomainDataSource, - dnsZone.NewZoneDataSource, - dnsRecordSet.NewRecordSetDataSource, - gitInstance.NewGitDataSource, - iaasAffinityGroup.NewAffinityGroupDatasource, - iaasImage.NewImageDataSource, - iaasImageV2.NewImageV2DataSource, - iaasNetwork.NewNetworkDataSource, - iaasNetworkArea.NewNetworkAreaDataSource, - iaasNetworkAreaRegion.NewNetworkAreaRegionDataSource, - iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource, - iaasNetworkInterface.NewNetworkInterfaceDataSource, - iaasVolume.NewVolumeDataSource, - iaasProject.NewProjectDataSource, - iaasPublicIp.NewPublicIpDataSource, - iaasPublicIpRanges.NewPublicIpRangesDataSource, - iaasKeyPair.NewKeyPairDataSource, - iaasServer.NewServerDataSource, - iaasSecurityGroup.NewSecurityGroupDataSource, - iaasalphaRoutingTable.NewRoutingTableDataSource, - iaasalphaRoutingTableRoute.NewRoutingTableRouteDataSource, - iaasalphaRoutingTables.NewRoutingTablesDataSource, - iaasalphaRoutingTableRoutes.NewRoutingTableRoutesDataSource, - iaasSecurityGroupRule.NewSecurityGroupRuleDataSource, - kmsKey.NewKeyDataSource, - kmsKeyRing.NewKeyRingDataSource, - kmsWrappingKey.NewWrappingKeyDataSource, - loadBalancer.NewLoadBalancerDataSource, - logMeInstance.NewInstanceDataSource, - logMeCredential.NewCredentialDataSource, - logAlertGroup.NewLogAlertGroupDataSource, - machineType.NewMachineTypeDataSource, - mariaDBInstance.NewInstanceDataSource, - mariaDBCredential.NewCredentialDataSource, - mongoDBFlexInstance.NewInstanceDataSource, - mongoDBFlexUser.NewUserDataSource, - objectStorageBucket.NewBucketDataSource, - objecStorageCredentialsGroup.NewCredentialsGroupDataSource, - objecStorageCredential.NewCredentialDataSource, - observabilityInstance.NewInstanceDataSource, - observabilityScrapeConfig.NewScrapeConfigDataSource, - openSearchInstance.NewInstanceDataSource, - openSearchCredential.NewCredentialDataSource, - postgresFlexDatabase.NewDatabaseDataSource, - postgresFlexInstance.NewInstanceDataSource, - postgresFlexUser.NewUserDataSource, - rabbitMQInstance.NewInstanceDataSource, - rabbitMQCredential.NewCredentialDataSource, - redisInstance.NewInstanceDataSource, - redisCredential.NewCredentialDataSource, - resourceManagerProject.NewProjectDataSource, - scfOrganization.NewScfOrganizationDataSource, - scfOrganizationmanager.NewScfOrganizationManagerDataSource, - scfPlatform.NewScfPlatformDataSource, - resourceManagerFolder.NewFolderDataSource, - secretsManagerInstance.NewInstanceDataSource, - secretsManagerUser.NewUserDataSource, - sqlServerFlexInstance.NewInstanceDataSource, - sqlServerFlexUser.NewUserDataSource, - serverBackupSchedule.NewScheduleDataSource, - serverBackupSchedule.NewSchedulesDataSource, - serverUpdateSchedule.NewScheduleDataSource, - serverUpdateSchedule.NewSchedulesDataSource, - serviceAccount.NewServiceAccountDataSource, - skeCluster.NewClusterDataSource, - } + return []func() datasource.DataSource{} } // Resources defines the resources implemented in the provider. func (p *Provider) Resources(_ context.Context) []func() resource.Resource { resources := []func() resource.Resource{ - alertGroup.NewAlertGroupResource, - cdn.NewDistributionResource, - cdnCustomDomain.NewCustomDomainResource, - dnsZone.NewZoneResource, - dnsRecordSet.NewRecordSetResource, - gitInstance.NewGitResource, - iaasAffinityGroup.NewAffinityGroupResource, - iaasImage.NewImageResource, - iaasNetwork.NewNetworkResource, - iaasNetworkArea.NewNetworkAreaResource, - iaasNetworkAreaRegion.NewNetworkAreaRegionResource, - iaasNetworkAreaRoute.NewNetworkAreaRouteResource, - iaasNetworkInterface.NewNetworkInterfaceResource, - iaasVolume.NewVolumeResource, - iaasPublicIp.NewPublicIpResource, - iaasKeyPair.NewKeyPairResource, - iaasVolumeAttach.NewVolumeAttachResource, - iaasNetworkInterfaceAttach.NewNetworkInterfaceAttachResource, - iaasServiceAccountAttach.NewServiceAccountAttachResource, - iaasPublicIpAssociate.NewPublicIpAssociateResource, - iaasServer.NewServerResource, - iaasSecurityGroup.NewSecurityGroupResource, - iaasSecurityGroupRule.NewSecurityGroupRuleResource, - iaasalphaRoutingTable.NewRoutingTableResource, - iaasalphaRoutingTableRoute.NewRoutingTableRouteResource, - kmsKey.NewKeyResource, - kmsKeyRing.NewKeyRingResource, - kmsWrappingKey.NewWrappingKeyResource, - loadBalancer.NewLoadBalancerResource, - loadBalancerObservabilityCredential.NewObservabilityCredentialResource, - logMeInstance.NewInstanceResource, - logMeCredential.NewCredentialResource, - logAlertGroup.NewLogAlertGroupResource, - mariaDBInstance.NewInstanceResource, - mariaDBCredential.NewCredentialResource, - modelServingToken.NewTokenResource, - mongoDBFlexInstance.NewInstanceResource, - mongoDBFlexUser.NewUserResource, - objectStorageBucket.NewBucketResource, - objecStorageCredentialsGroup.NewCredentialsGroupResource, - objecStorageCredential.NewCredentialResource, - observabilityCredential.NewCredentialResource, - observabilityInstance.NewInstanceResource, - observabilityScrapeConfig.NewScrapeConfigResource, - openSearchInstance.NewInstanceResource, - openSearchCredential.NewCredentialResource, - postgresFlexDatabase.NewDatabaseResource, - postgresFlexInstance.NewInstanceResource, postgresFlexAlphaInstance.NewInstanceResource, - postgresFlexUser.NewUserResource, - rabbitMQInstance.NewInstanceResource, - rabbitMQCredential.NewCredentialResource, - redisInstance.NewInstanceResource, - redisCredential.NewCredentialResource, - resourceManagerProject.NewProjectResource, - scfOrganization.NewScfOrganizationResource, - scfOrganizationmanager.NewScfOrganizationManagerResource, - resourceManagerFolder.NewFolderResource, - secretsManagerInstance.NewInstanceResource, - secretsManagerUser.NewUserResource, - sqlServerFlexInstance.NewInstanceResource, - sqlServerFlexUser.NewUserResource, - serverBackupSchedule.NewScheduleResource, - serverUpdateSchedule.NewScheduleResource, - serviceAccount.NewServiceAccountResource, - serviceAccountToken.NewServiceAccountTokenResource, - serviceAccountKey.NewServiceAccountKeyResource, - skeCluster.NewClusterResource, - skeKubeconfig.NewKubeconfigResource, } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) return resources } - -// EphemeralResources defines the ephemeral resources implemented in the provider. -func (p *Provider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { - return []func() ephemeral.EphemeralResource{ - access_token.NewAccessTokenEphemeralResource, - } -}