diff --git a/docs/resources/docker.md b/docs/resources/docker.md index 5b703853..bcd2902b 100644 --- a/docs/resources/docker.md +++ b/docs/resources/docker.md @@ -44,6 +44,7 @@ resource "clevercloud_docker" "docker_instance" { - `app_folder` (String) Folder in which the application is located (inside the git repository) - `build_flavor` (String) Use dedicated instance with given flavor for build phase +- `buildx` (Boolean) Set to true to use buildx to build the Docker image - `container_port` (Number) Set to custom HTTP port if your Docker container runs on custom port - `container_port_tcp` (Number) Set to custom TCP port if your Docker container runs on custom port. - `daemon_socket_mount` (Boolean) Set to true to access the host Docker socket from inside your container diff --git a/docs/resources/frankenphp.md b/docs/resources/frankenphp.md index 8b56d701..87cabbf3 100644 --- a/docs/resources/frankenphp.md +++ b/docs/resources/frankenphp.md @@ -36,6 +36,7 @@ FrankenPHP is a modern PHP application server, written in Go. It gives superpowe - `app_folder` (String) Folder in which the application is located (inside the git repository) - `build_flavor` (String) Use dedicated instance with given flavor for build phase +- `composer_flags` (String) Flags to pass to Composer - `dependencies` (Set of String) A list of application or add-ons required to run this application. Can be either app_xxx or postgres_yyy ID format - `deployment` (Block, Optional) (see [below for nested schema](#nestedblock--deployment)) @@ -43,10 +44,13 @@ Can be either app_xxx or postgres_yyy ID format - `dev_dependencies` (Boolean) Install development dependencies (Default: false) - `environment` (Map of String, Sensitive) Environment variables injected into the application - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) +- `listened_port` (Number) The port on which FrankenPHP listens for HTTP requests - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) +- `webroot` (String) Path to the web content to serve, relative to the root of your application +- `worker_path` (String) Path to the worker script, relative to the root of your project (e.g. /worker/scrip.php) ### Read-Only diff --git a/docs/resources/go.md b/docs/resources/go.md index b4da1aa3..d4ebcc86 100644 --- a/docs/resources/go.md +++ b/docs/resources/go.md @@ -96,6 +96,9 @@ Can be either app_xxx or postgres_yyy ID format - `deployment` (Block, Optional) (see [below for nested schema](#nestedblock--deployment)) - `description` (String) Application description - `environment` (Map of String, Sensitive) Environment variables injected into the application +- `go_build_tool` (String) Available values: `gomod`, `gobuild`. Build and install your application (`goget` is deprecated) +- `go_pkg` (String) Tell the `CC_GO_BUILD_TOOL` which file contains the `main()` function (default: `main.go`) +- `go_rundir` (String) Run the application from the specified path, relative to `$GOPATH/src/` (deprecated) - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed diff --git a/docs/resources/java_war.md b/docs/resources/java_war.md index 0fe6dd80..29fd4a6d 100644 --- a/docs/resources/java_war.md +++ b/docs/resources/java_war.md @@ -95,11 +95,24 @@ resource "clevercloud_java_war" "myapp" { Can be either app_xxx or postgres_yyy ID format - `deployment` (Block, Optional) (see [below for nested schema](#nestedblock--deployment)) - `description` (String) Application description +- `disable_max_metaspace` (Boolean) Allows to disable the Java option `-XX:MaxMetaspaceSize` - `environment` (Map of String, Sensitive) Environment variables injected into the application +- `extra_java_args` (String) Define extra arguments to pass to `java` for JAR +- `gradle_deploy_goal` (String) Define which Gradle goals to run during build - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) -- `java_version` (String) Choose the JVM version between 7 to 24 for OpenJDK or graalvm-ce for GraalVM 21.0.0.2 (based on OpenJDK 11.0). +- `jar_args` (String) Define arguments to pass to the launched JAR +- `jar_path` (String) Define the path to your JAR +- `java_version` (String) Choose the JVM version between 7 to 24 for OpenJDK or `graalvm-ce` for GraalVM (default: 21) +- `maven_deploy_goal` (String) Define which Maven goals to run during build +- `maven_profiles` (String) Define which Maven profile to use during default build +- `nudge_app_id` (String) Nudge application ID +- `play1_version` (String) Define which play1 version to use between `1.2`, `1.3`, `1.4` and `1.5` - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed +- `run_command` (String) Custom command to run your application. Replaces the default behavior +- `sbt_deploy_goal` (String) Define which SBT goals to run during build (default: `stage`) +- `sbt_target_bin` (String) Define the bin to pick in the `CC_SBT_TARGET_DIR` +- `sbt_target_dir` (String) Define the folder the `target` dir is in (default: `.`) - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) diff --git a/docs/resources/nodejs.md b/docs/resources/nodejs.md index 6eb665b6..a7a9f008 100644 --- a/docs/resources/nodejs.md +++ b/docs/resources/nodejs.md @@ -91,6 +91,7 @@ resource "clevercloud_nodejs" "myapp" { - `app_folder` (String) Folder in which the application is located (inside the git repository) - `build_flavor` (String) Use dedicated instance with given flavor for build phase +- `custom_build_tool` (String) A custom command to run (with package_manager set to `custom`) - `dependencies` (Set of String) A list of application or add-ons required to run this application. Can be either app_xxx or postgres_yyy ID format - `deployment` (Block, Optional) (see [below for nested schema](#nestedblock--deployment)) @@ -98,10 +99,12 @@ Can be either app_xxx or postgres_yyy ID format - `dev_dependencies` (Boolean) Install development dependencies specified in package.json - `environment` (Map of String, Sensitive) Environment variables injected into the application - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) -- `package_manager` (String) Either npm, npm-ci, bun, pnpm, yarn-berry or custom +- `node_version` (String) Set Node.js version, for example `24`, `23.11` or `22.15.1` +- `package_manager` (String) Choose your build tool between npm, npm-ci, yarn, yarn2 and custom. Default is `npm` - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed -- `registry` (String) The host of your private repository, available values: github or the registry host +- `registry` (String) The host of your private repository, available values: github or the registry host. Default is `registry.npmjs.org` +- `registry_basic_auth` (String, Sensitive) Private repository credentials, in the form `user:password`. You can't use this if registry_token is set - `registry_token` (String, Sensitive) Private repository token - `start_script` (String) Set custom start script, instead of `npm start` - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped diff --git a/docs/resources/php.md b/docs/resources/php.md index 21c381c9..9524f549 100644 --- a/docs/resources/php.md +++ b/docs/resources/php.md @@ -27,22 +27,52 @@ See [PHP with Apache product specification](https://www.clever.cloud/developers/ ### Optional +- `always_populate_raw_post_data` (String) Controls population of raw POST data +- `apache_headers_size` (Number) Set the maximum size of the headers in Apache, between `8` and `256`. Default is `8` - `app_folder` (String) Folder in which the application is located (inside the git repository) +- `async_app_bucket` (String) Mount the default app FS bucket asynchronously. If set, should have value `async` - `build_flavor` (String) Use dedicated instance with given flavor for build phase +- `cgi_implementation` (String) Choose the Apache FastCGI module between `fastcgi` and `proxy_fcgi`. Default is `proxy_fcgi` +- `composer_version` (String) Choose your composer version between 1 and 2. Default is `2` - `dependencies` (Set of String) A list of application or add-ons required to run this application. Can be either app_xxx or postgres_yyy ID format - `deployment` (Block, Optional) (see [below for nested schema](#nestedblock--deployment)) - `description` (String) Application description -- `dev_dependencies` (Boolean) Install development dependencies +- `dev_dependencies` (String) Control if development dependencies are installed or not. Values are either `install` or `ignore` +- `disable_app_bucket` (String) Disable entirely the app FS Bucket. Values are either `true`, `yes` or `disable` +- `enable_elastic_apm_agent` (Boolean) Enable the Elastic APM Agent for PHP. Default is `true` if `ELASTIC_APM_SERVER_URL` is defined, `false` otherwise +- `enable_grpc` (Boolean) Enable the use of gRPC module. Default is `false` +- `enable_pdflib` (Boolean) Enable the use of PDFlib module. Default is `false` +- `enable_redis` (Boolean) Enable Redis support. Default is `false` - `environment` (Map of String, Sensitive) Environment variables injected into the application - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) -- `php_version` (String) PHP version (Default: 8) +- `http_basic_auth` (String, Sensitive) Restrict HTTP access to your application. Example: `login:password`. You can define multiple credentials using additional `CC_HTTP_BASIC_AUTH_n` (where `n` is a number) environment variables +- `http_timeout` (Number) Define a custom HTTP timeout. Default is `180` +- `ldap_ca_cert` (String) Path to the LDAP CA certificate +- `ldaptls_cacert` (String) Path to the LDAP TLS CA certificate +- `max_input_vars` (Number) Maximum number of input variables that can be accepted +- `memory_limit` (String) Change the default memory limit for PHP scripts +- `mta_auth_password` (String, Sensitive) Password to authenticate to the SMTP server +- `mta_auth_user` (String) User to authenticate to the SMTP server +- `mta_server_auth_method` (String) Enable or disable authentication to the SMTP server. Default is `on` +- `mta_server_host` (String) Host of the SMTP server +- `mta_server_port` (Number) Port of the SMTP server. Default is `465` +- `mta_server_use_tls` (Boolean) Enable or disable TLS when connecting to the SMTP server. Default is `true` +- `opcache_interned_strings_buffer` (Number) The amount of memory used to store interned strings, in megabytes. Default is `4` (PHP5), `8` (PHP7) +- `opcache_max_accelerated_files` (Number) Maximum number of files handled by opcache. Default depends on the scaler size +- `opcache_memory` (String) Set the shared opcache memory size. Default is about 1/8 of the RAM +- `opcache_preload` (String) The path of the PHP preload file (PHP version 7.4 or higher) +- `php_version` (String) Choose your PHP version among those supported. Default is `8.3` +- `realpath_cache_ttl` (Number) The size of the realpath cache to be used by PHP. Default is `120` - `redirect_https` (Boolean) Redirect client from plain to TLS port -- `redis_sessions` (Boolean) Use a linked Redis instance to store sessions (Default: false) - `region` (String) Geographical region where the database will be deployed +- `session_type` (String) Choose `redis` to use Redis as session store +- `socksify_everything` (Boolean) Enable SOCKS proxy for all outgoing connections. Default is `false` +- `sqreen_api_app_name` (String) The name of your Sqreen application +- `sqreen_api_token` (String, Sensitive) Your Sqreen organization token - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) -- `webroot` (String) Define the DocumentRoot of your project (default: ".") +- `webroot` (String) Define the DocumentRoot of your project. Default is `.` ### Read-Only diff --git a/docs/resources/play2.md b/docs/resources/play2.md index 05ebe91c..a4fe7e39 100644 --- a/docs/resources/play2.md +++ b/docs/resources/play2.md @@ -97,8 +97,12 @@ Can be either app_xxx or postgres_yyy ID format - `description` (String) Application description - `environment` (Map of String, Sensitive) Environment variables injected into the application - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) +- `play1_version` (String) Define which play1 version to use between `1.2`, `1.3`, `1.4` and `1.5` - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed +- `sbt_deploy_goal` (String) Define which SBT goals to run during build (default: `stage`) +- `sbt_target_bin` (String) Define the bin to pick in the `CC_SBT_TARGET_DIR` +- `sbt_target_dir` (String) Define the folder the `target` dir is in (default: `.`) - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) diff --git a/docs/resources/python.md b/docs/resources/python.md index be963554..e21f0fa6 100644 --- a/docs/resources/python.md +++ b/docs/resources/python.md @@ -91,18 +91,45 @@ resource "clevercloud_python" "myapp" { - `app_folder` (String) Folder in which the application is located (inside the git repository) - `build_flavor` (String) Use dedicated instance with given flavor for build phase +- `celery_logfile` (String) Sets the relative path to the Celery log file (e.g., `/path/to/logdir`) +- `celery_module` (String) Specifies the Celery module to start +- `celery_use_beat` (Boolean) Set to `true` to enable Celery Beat support - `dependencies` (Set of String) A list of application or add-ons required to run this application. Can be either app_xxx or postgres_yyy ID format - `deployment` (Block, Optional) (see [below for nested schema](#nestedblock--deployment)) - `description` (String) Application description +- `enable_gzip_compression` (Boolean) Set to `true` to enable Gzip compression via Nginx - `environment` (Map of String, Sensitive) Environment variables injected into the application +- `gunicorn_timeout` (Number) Timeout for Gunicorn workers. Default is `180` +- `gunicorn_worker_class` (String) Gunicorn worker class (e.g., `gevent`, `sync`) +- `gzip_types` (String) Defines the MIME types to be compressed by Gzip. Default is `text/* application/json application/xml application/javascript image/svg+xml` +- `harakiri` (Number) Timeout in seconds after which an unresponsive process is killed. Default is `180` - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) -- `pip_requirements` (String) Define a custom requirements.txt file (default: requirements.txt) -- `python_version` (String) Python version >= 2.7 +- `http_basic_auth` (String, Sensitive) Restrict HTTP access to your application. Example: `login:password`. Multiple credentials can be defined using `CC_HTTP_BASIC_AUTH_n` +- `manage_tasks` (String) A comma-separated list of Django `manage.py` tasks to execute +- `nginx_proxy_buffer_size` (String) Sets the size of the buffer for the initial part of the response from the proxied server +- `nginx_proxy_buffers` (String) Configures the number and size of buffers for reading responses from the proxied server +- `nginx_read_timeout` (Number) Read timeout in seconds for Nginx. Default is `300` +- `pip_requirements` (String) Specifies a custom requirements.txt file for package installation. Default is `requirements.txt` +- `python_backend` (String) Selects the Python backend. Options include `daphne`, `gunicorn`, `uvicorn`, and `uwsgi`. Default is `uwsgi` +- `python_module` (String) Defines the Python module to start with, including the path to the application object. Example: `app.server:app` for a `server.py` file in an `/app` folder +- `python_version` (String) Selects the Python version. Refer to supported versions documentation - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed +- `setup_py_goal` (String) A custom goal to execute after `requirements.txt` installation +- `static_files_path` (String) The relative path to the directory containing static files (e.g., `path/to/static`) +- `static_url_prefix` (String) The URL path prefix for serving static files. Commonly set to `/public` +- `static_webroot` (String) Specifies the web root for static files - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped +- `use_gevent` (Boolean) Set to `true` to enable Gevent support +- `uwsgi_async` (Number) Configures the number of cores for uWSGI asynchronous/non-blocking modes +- `uwsgi_async_engine` (String) Selects the asynchronous engine for uWSGI (optional) +- `uwsgi_intercept_errors` (Boolean) Enables or disables error interception in uWSGI - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) +- `wsgi_buffer_size` (Number) Buffer size in bytes for uploads. Default is `4096` +- `wsgi_post_buffering` (Number) Maximum size in bytes for request headers. Default is `4096` +- `wsgi_threads` (Number) Number of threads per worker. Defaults to automatic setup based on scaler size +- `wsgi_workers` (Number) Number of workers. Defaults to automatic setup based on scaler size ### Read-Only diff --git a/docs/resources/rust.md b/docs/resources/rust.md index 4df8087b..7c9c2788 100644 --- a/docs/resources/rust.md +++ b/docs/resources/rust.md @@ -100,6 +100,9 @@ Can be either app_xxx or postgres_yyy ID format - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed +- `run_command` (String) Custom command to run your application +- `rust_bin` (String) The name of the binary to launch once built +- `rustup_channel` (String) The rust channel to use. Use a specific channel version with `stable`, `beta`, `nightly` or a specific version like `1.13.0` (default: `stable`) - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) diff --git a/docs/resources/scala.md b/docs/resources/scala.md index c20bbcd5..047bce53 100644 --- a/docs/resources/scala.md +++ b/docs/resources/scala.md @@ -99,6 +99,9 @@ Can be either app_xxx or postgres_yyy ID format - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed +- `sbt_deploy_goal` (String) Define which SBT goals to run during build (default: `stage`) +- `sbt_target_bin` (String) Define the bin to pick in the `CC_SBT_TARGET_DIR` +- `sbt_target_dir` (String) Define the folder the `target` dir is in (default: `.`) - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) diff --git a/docs/resources/static.md b/docs/resources/static.md index 8c9b0ae1..42d1c245 100644 --- a/docs/resources/static.md +++ b/docs/resources/static.md @@ -90,6 +90,7 @@ resource "clevercloud_static" "myapp" { ### Optional - `app_folder` (String) Folder in which the application is located (inside the git repository) +- `build_command` (String) Command to run during build phase - `build_flavor` (String) Use dedicated instance with given flavor for build phase - `dependencies` (Set of String) A list of application or add-ons required to run this application. Can be either app_xxx or postgres_yyy ID format @@ -97,10 +98,18 @@ Can be either app_xxx or postgres_yyy ID format - `description` (String) Application description - `environment` (Map of String, Sensitive) Environment variables injected into the application - `hooks` (Block, Optional) (see [below for nested schema](#nestedblock--hooks)) +- `hugo_version` (String) Set Hugo version (e.g., `0.150`) +- `override_buildcache` (String) Customize build cache directories - `redirect_https` (Boolean) Redirect client from plain to TLS port - `region` (String) Geographical region where the database will be deployed +- `static_autobuild_outdir` (String) Output directory for static site generator (default: `/cc_static_autobuilt`) +- `static_caddyfile` (String) Path to Caddyfile for custom Caddy configuration (default: `./Caddyfile`) +- `static_flags` (String) Custom command line flags to pass to the static server +- `static_port` (Number) Custom listen port for the static server (default: `8080`) +- `static_server` (String) Server to use for static website (default: `static-web-server`) - `sticky_sessions` (Boolean) Enable sticky sessions, use it when your client sessions are instances scoped - `vhosts` (Attributes Set) List of virtual hosts (see [below for nested schema](#nestedatt--vhosts)) +- `webroot` (String) Path to web content to serve (default: `/`) ### Read-Only diff --git a/go.mod b/go.mod index ea106a44..5b471719 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go.clever-cloud.com/terraform-provider -go 1.23.7 +go 1.24.0 toolchain go1.24.5 diff --git a/pkg/attributes/addon.go b/pkg/attributes/addon.go index c1dbb616..69fcfcde 100644 --- a/pkg/attributes/addon.go +++ b/pkg/attributes/addon.go @@ -6,8 +6,10 @@ import ( "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" "go.clever-cloud.com/terraform-provider/pkg" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Addon struct { @@ -21,7 +23,11 @@ type Addon struct { var addonCommon = map[string]schema.Attribute{ "id": schema.StringAttribute{Computed: true, MarkdownDescription: "Generated unique identifier", PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}}, "name": schema.StringAttribute{Required: true, MarkdownDescription: "Name of the service"}, - "plan": schema.StringAttribute{Required: true, MarkdownDescription: "Database size and spec"}, + "plan": schema.StringAttribute{ + Required: true, + MarkdownDescription: "Database size and spec", + Validators: []validator.String{helper.CCPlanFlavorValidator}, + }, "region": schema.StringAttribute{ Optional: true, Computed: true, diff --git a/pkg/attributes/blocks.go b/pkg/attributes/blocks.go index cfb30783..f5d8fbae 100644 --- a/pkg/attributes/blocks.go +++ b/pkg/attributes/blocks.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "go.clever-cloud.com/terraform-provider/pkg" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) // Deployment block @@ -26,6 +27,14 @@ type Hooks struct { RunFailed types.String `tfsdk:"run_failed"` } +const ( + CC_PRE_BUILD_HOOK = "CC_PRE_BUILD_HOOK" + CC_POST_BUILD_HOOK = "CC_POST_BUILD_HOOK" + CC_PRE_RUN_HOOK = "CC_PRE_RUN_HOOK" + CC_RUN_FAILED_HOOK = "CC_RUN_FAILED_HOOK" + CC_RUN_SUCCEEDED_HOOK = "CC_RUN_SUCCEEDED_HOOK" +) + var blocks = map[string]schema.Block{ "deployment": schema.SingleNestedBlock{ Attributes: map[string]schema.Attribute{ @@ -111,11 +120,33 @@ func (hooks *Hooks) ToEnv() map[string]string { return m } - pkg.IfIsSetStr(hooks.PreBuild, func(script string) { m["CC_PRE_BUILD_HOOK"] = script }) - pkg.IfIsSetStr(hooks.PostBuild, func(script string) { m["CC_POST_BUILD_HOOK"] = script }) - pkg.IfIsSetStr(hooks.PreRun, func(script string) { m["CC_PRE_RUN_HOOK"] = script }) - pkg.IfIsSetStr(hooks.RunFailed, func(script string) { m["CC_RUN_FAILED_HOOK"] = script }) - pkg.IfIsSetStr(hooks.RunSucceed, func(script string) { m["CC_RUN_SUCCEEDED_HOOK"] = script }) + pkg.IfIsSetStr(hooks.PreBuild, func(script string) { m[CC_PRE_BUILD_HOOK] = script }) + pkg.IfIsSetStr(hooks.PostBuild, func(script string) { m[CC_POST_BUILD_HOOK] = script }) + pkg.IfIsSetStr(hooks.PreRun, func(script string) { m[CC_PRE_RUN_HOOK] = script }) + pkg.IfIsSetStr(hooks.RunFailed, func(script string) { m[CC_RUN_FAILED_HOOK] = script }) + pkg.IfIsSetStr(hooks.RunSucceed, func(script string) { m[CC_RUN_SUCCEEDED_HOOK] = script }) return m } + +func (hooks *Hooks) FromEnv(env *helper.EnvMap) { + if script := env.Pop(CC_PRE_BUILD_HOOK); script != "" { + hooks.PreBuild = pkg.FromStr(script) + } + + if script := env.Pop(CC_POST_BUILD_HOOK); script != "" { + hooks.PostBuild = pkg.FromStr(script) + } + + if script := env.Pop(CC_PRE_RUN_HOOK); script != "" { + hooks.PreRun = pkg.FromStr(script) + } + + if script := env.Pop(CC_RUN_SUCCEEDED_HOOK); script != "" { + hooks.RunSucceed = pkg.FromStr(script) + } + + if script := env.Pop(CC_RUN_FAILED_HOOK); script != "" { + hooks.RunFailed = pkg.FromStr(script) + } +} diff --git a/pkg/attributes/runtime.go b/pkg/attributes/runtime.go index fc939882..794f8251 100644 --- a/pkg/attributes/runtime.go +++ b/pkg/attributes/runtime.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -24,6 +25,8 @@ type VHost struct { PathBegin types.String `tfsdk:"path_begin"` } +const APP_FOLDER = "APP_FOLDER" + func (vh VHost) String() *string { if vh.FQDN.IsNull() || vh.FQDN.IsUnknown() { return nil @@ -61,6 +64,47 @@ type Runtime struct { Environment types.Map `tfsdk:"environment"` } +func (r *Runtime) FromEnvironment(ctx context.Context, env *helper.EnvMap) { + r.Hooks.FromEnv(env) + + if appFolder := env.Pop(APP_FOLDER); appFolder != "" { + r.AppFolder = pkg.FromStr(appFolder) + } + + if env.Size() == 0 { + return + } + + m := map[string]attr.Value{} + for k, v := range env.All { + m[k] = pkg.FromStr(v) + } + r.Environment = types.MapValueMust(types.StringType, m) +} + +// ToEnv converts common Runtime fields to environment variables map +// This includes APP_FOLDER, Hooks, and custom Environment variables +func (r *Runtime) ToEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { + env := map[string]string{} + + // Custom environment variables from user config + // Bug workaround: https://github.com/hashicorp/terraform-plugin-framework/issues/698 + customEnv := map[string]string{} + diags.Append(r.Environment.ElementsAs(ctx, &customEnv, false)...) + if diags.HasError() { + return env + } + env = pkg.Merge(env, customEnv) + + // APP_FOLDER + pkg.IfIsSetStr(r.AppFolder, func(s string) { env[APP_FOLDER] = s }) + + // Hooks + env = pkg.Merge(env, r.Hooks.ToEnv()) + + return env +} + type RuntimeV0 struct { ID types.String `tfsdk:"id"` Name types.String `tfsdk:"name"` @@ -130,17 +174,17 @@ var runtimeCommon = map[string]schema.Attribute{ }, "smallest_flavor": schema.StringAttribute{ Required: true, - Validators: []validator.String{helper.UpperCaseValidator}, + Validators: []validator.String{helper.CCPlanFlavorValidator}, MarkdownDescription: "Smallest instance flavor", }, "biggest_flavor": schema.StringAttribute{ Required: true, - Validators: []validator.String{helper.UpperCaseValidator}, + Validators: []validator.String{helper.CCPlanFlavorValidator}, MarkdownDescription: "Biggest instance flavor, if different from smallest, enable auto-scaling", }, "build_flavor": schema.StringAttribute{ Optional: true, - Validators: []validator.String{helper.UpperCaseValidator}, + Validators: []validator.String{helper.CCPlanFlavorValidator}, MarkdownDescription: "Use dedicated instance with given flavor for build phase", }, "region": schema.StringAttribute{ diff --git a/pkg/helper/envmap.go b/pkg/helper/envmap.go new file mode 100644 index 00000000..ce2c6caf --- /dev/null +++ b/pkg/helper/envmap.go @@ -0,0 +1,32 @@ +package helper + +// EnvMap is a wrapper around map[string]string that provides +// a Pop method to retrieve and remove keys in one operation. +// This is useful for parsing environment variables where you want +// to extract known keys and keep the rest. +type EnvMap struct { + All map[string]string +} + +// NewEnvMap creates a new EnvMap from a map[string]string. +// The input map is copied to avoid modifying the original. +func NewEnvMap(m map[string]string) *EnvMap { + copied := make(map[string]string, len(m)) + for k, v := range m { + copied[k] = v + } + return &EnvMap{All: copied} +} + +// Pop retrieves the value for the given key and removes it from the map. +// Returns an empty string if the key doesn't exist. +func (em *EnvMap) Pop(key string) string { + value := em.All[key] + delete(em.All, key) + return value +} + +// Size returns the number of remaining entries in the map. +func (em *EnvMap) Size() int { + return len(em.All) +} diff --git a/pkg/helper/planmodifier.go b/pkg/helper/validator.go similarity index 53% rename from pkg/helper/planmodifier.go rename to pkg/helper/validator.go index 2f4b08cc..6943f7b9 100644 --- a/pkg/helper/planmodifier.go +++ b/pkg/helper/validator.go @@ -2,25 +2,28 @@ package helper import ( "context" - "strings" + "regexp" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "go.clever-cloud.com/terraform-provider/pkg" ) -var UpperCaseValidator = pkg.NewStringValidator( - "Uppercase letters only", +// https://regex101.com/r/bMOotf/1 +var planRegex = regexp.MustCompile(`^[a-zA-Z_]*$`) +var CCPlanFlavorValidator = pkg.NewStringValidator( + "Expect CleverCloud plan flavor only", func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { return } - v := req.ConfigValue.ValueString() - if strings.ToUpper(v) != v { + if !planRegex.MatchString(req.ConfigValue.ValueString()) { resp.Diagnostics.AddAttributeError( req.Path, "Invalid value", - "Expect uppercase letters only", + "Expect letters and underscores only", ) } + + // TODO: check if plan flavor exists in CC plan flavor list }) diff --git a/pkg/resources/addon/crud.go b/pkg/resources/addon/crud.go index 99e5fca7..39d6ea3c 100644 --- a/pkg/resources/addon/crud.go +++ b/pkg/resources/addon/crud.go @@ -105,7 +105,7 @@ func (r *ResourceAddon) Read(ctx context.Context, req resource.ReadRequest, resp a := addonRes.Payload() ad.Name = pkg.FromStr(a.Name) - ad.Plan = pkg.FromStr(a.Plan.Slug) + ad.Plan = pkg.FromStr(strings.ToLower(a.Plan.Slug)) ad.Region = pkg.FromStr(a.Region) ad.ThirdPartyProvider = pkg.FromStr(a.Provider.ID) ad.CreationDate = pkg.FromI(a.CreationDate) diff --git a/pkg/resources/cellar/bucket/bucket_test.go b/pkg/resources/cellar/bucket/bucket_test.go index 1cb02524..d1b77150 100644 --- a/pkg/resources/cellar/bucket/bucket_test.go +++ b/pkg/resources/cellar/bucket/bucket_test.go @@ -4,7 +4,6 @@ import ( "context" _ "embed" "fmt" - "os" "testing" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -20,34 +19,29 @@ import ( ) func TestAccCellarBucket_basic(t *testing.T) { - t.Parallel() ctx := context.Background() rName := acctest.RandomWithPrefix("my-bucket") cc := client.New(client.WithAutoOauthConfig()) providerBlock := helper.NewProvider("clevercloud").SetOrganisation(tests.ORGANISATION) - cellar := &tmp.AddonResponse{} - if os.Getenv("TF_ACC") == "1" { - res := tmp.CreateAddon(ctx, cc, tests.ORGANISATION, tmp.AddonRequest{ - Name: acctest.RandomWithPrefix("tf-cellar-forbucket"), - ProviderID: "cellar-addon", - Plan: "plan_84c85ee3-5fdb-4aca-a727-298ddc14b766", - Region: "par", - }) - if res.HasError() { - t.Errorf("failed to create depdendence Cellar: %s", res.Error().Error()) - return - } - - cellar = res.Payload() - - defer func() { - rmRes := tmp.DeleteAddon(ctx, cc, tests.ORGANISATION, cellar.ID) - if rmRes.HasError() && !rmRes.IsNotFoundError() { - t.Fatalf("failed to destroy deps %s: %s", cellar.RealID, rmRes.Error().Error()) - } - }() + res := tmp.CreateAddon(ctx, cc, tests.ORGANISATION, tmp.AddonRequest{ + Name: acctest.RandomWithPrefix("tf-cellar-forbucket"), + ProviderID: "cellar-addon", + Plan: "plan_84c85ee3-5fdb-4aca-a727-298ddc14b766", + Region: "par", + }) + if res.HasError() { + t.Errorf("failed to create depdendence Cellar: %s", res.Error().Error()) + return } + cellar := res.Payload() + + defer func() { + rmRes := tmp.DeleteAddon(ctx, cc, tests.ORGANISATION, cellar.ID) + if rmRes.HasError() && !rmRes.IsNotFoundError() { + t.Fatalf("failed to destroy deps %s: %s", cellar.RealID, rmRes.Error().Error()) + } + }() cellarBucketBlock := helper.NewRessource( "clevercloud_cellar_bucket", @@ -80,17 +74,17 @@ func TestAccCellarBucket_basic(t *testing.T) { continue } if res.HasError() { - return fmt.Errorf("unexpectd error: %s", res.Error().Error()) + return fmt.Errorf("unexpectd error: %w", res.Error()) } minioClient, err := s3.MinioClientFromEnvsFor(*res.Payload()) if err != nil { - return fmt.Errorf("unexpectd error: %s", res.Error().Error()) + return fmt.Errorf("unexpectd error: %w", res.Error()) } exists, err := minioClient.BucketExists(ctx, rName) if err != nil { - return fmt.Errorf("unexpectd error: %s", res.Error().Error()) + return fmt.Errorf("unexpectd error: %w", res.Error()) } if exists { diff --git a/pkg/resources/configprovider/configprovider_test.go b/pkg/resources/configprovider/configprovider_test.go index 1c2d8ddb..62a0ccb5 100644 --- a/pkg/resources/configprovider/configprovider_test.go +++ b/pkg/resources/configprovider/configprovider_test.go @@ -38,7 +38,7 @@ func TestAccConfigProvider_basic(t *testing.T) { retrieveConfigProvider := func(ctx context.Context, id string) (*tmp.ConfigProvider, error) { res := tmp.GetConfigProvider(ctx, cc, id) if res.IsNotFoundError() { - return nil, fmt.Errorf("Unable to find configProvider by real id " + id) + return nil, fmt.Errorf("Unable to find configProvider by real id %s", id) } if res.HasError() { return nil, fmt.Errorf("Unexpectd error: %s", res.Error().Error()) diff --git a/pkg/resources/configprovider/crud.go b/pkg/resources/configprovider/crud.go index 7895ea84..ec81f983 100644 --- a/pkg/resources/configprovider/crud.go +++ b/pkg/resources/configprovider/crud.go @@ -124,8 +124,14 @@ func (r *ResourceConfigProvider) Update(ctx context.Context, req resource.Update return } + addonId, err := tmp.RealIDToAddonID(ctx, r.Client(), r.Organization(), plan.ID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("failed to get addon ID", err.Error()) + return + } + // Only name can be edited - addonRes := tmp.UpdateAddon(ctx, r.Client(), r.Organization(), plan.ID.ValueString(), map[string]string{ + addonRes := tmp.UpdateAddon(ctx, r.Client(), r.Organization(), addonId, map[string]string{ "name": plan.Name.ValueString(), }) if addonRes.HasError() { diff --git a/pkg/resources/docker/crud.go b/pkg/resources/docker/crud.go index 3a360598..a6b9136c 100644 --- a/pkg/resources/docker/crud.go +++ b/pkg/resources/docker/crud.go @@ -14,9 +14,8 @@ import ( // Create a new resource func (r *ResourceDocker) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - plan := Docker{} - - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + tflog.Debug(ctx, "ResourceDocker.Create()") + plan := helper.PlanFrom[Docker](ctx, req.Plan, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -27,6 +26,9 @@ func (r *ResourceDocker) Create(ctx context.Context, req resource.CreateRequest, } vhosts := plan.VHostsAsStrings(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } environment := plan.toEnv(ctx, &resp.Diagnostics) if resp.Diagnostics.HasError() { @@ -84,9 +86,8 @@ func (r *ResourceDocker) Create(ctx context.Context, req resource.CreateRequest, // Read resource information func (r *ResourceDocker) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var state Docker - - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + tflog.Debug(ctx, "ResourceDocker.Read()") + state := helper.StateFrom[Docker](ctx, req.State, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -110,7 +111,7 @@ func (r *ResourceDocker) Read(ctx context.Context, req resource.ReadRequest, res state.Region = pkg.FromStr(app.App.Zone) state.DeployURL = pkg.FromStr(app.App.DeployURL) state.BuildFlavor = app.GetBuildFlavor() - + state.fromEnv(ctx, app.EnvAsMap()) state.VHosts = helper.VHostsFromAPIHosts(ctx, app.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) @@ -188,14 +189,16 @@ func (r *ResourceDocker) Update(ctx context.Context, req resource.UpdateRequest, plan.VHosts = helper.VHostsFromAPIHosts(ctx, updatedApp.Application.Vhosts.AsString(), plan.VHosts, &res.Diagnostics) + plan.ID = pkg.FromStr(updatedApp.Application.ID) + plan.DeployURL = pkg.FromStr(updatedApp.Application.DeployURL) + res.Diagnostics.Append(res.State.Set(ctx, plan)...) } // Delete resource func (r *ResourceDocker) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var state Docker - - resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + tflog.Debug(ctx, "ResourceDocker.Delete()") + state := helper.StateFrom[Docker](ctx, req.State, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } diff --git a/pkg/resources/docker/docker.go b/pkg/resources/docker/docker.go index bf77b977..e1592864 100644 --- a/pkg/resources/docker/docker.go +++ b/pkg/resources/docker/docker.go @@ -14,6 +14,18 @@ type ResourceDocker struct { helper.Configurer } +const ( + CC_DOCKERFILE = "CC_DOCKERFILE" + CC_DOCKER_BUILDX = "CC_DOCKER_BUILDX" + CC_DOCKER_EXPOSED_HTTP_PORT = "CC_DOCKER_EXPOSED_HTTP_PORT" + CC_DOCKER_EXPOSED_TCP_PORT = "CC_DOCKER_EXPOSED_TCP_PORT" + CC_DOCKER_FIXED_CIDR_V6 = "CC_DOCKER_FIXED_CIDR_V6" + CC_DOCKER_LOGIN_SERVER = "CC_DOCKER_LOGIN_SERVER" + CC_DOCKER_LOGIN_USERNAME = "CC_DOCKER_LOGIN_USERNAME" + CC_DOCKER_LOGIN_PASSWORD = "CC_DOCKER_LOGIN_PASSWORD" + CC_MOUNT_DOCKER_SOCKET = "CC_MOUNT_DOCKER_SOCKET" +) + func NewResourceDocker() resource.Resource { return &ResourceDocker{} } @@ -29,7 +41,7 @@ func (r *ResourceDocker) UpgradeState(ctx context.Context) map[int64]resource.St 0: { PriorSchema: &schemaDockerV0, StateUpgrader: func(ctx context.Context, req resource.UpgradeStateRequest, res *resource.UpgradeStateResponse) { - old := helper.StateFrom[Docker](ctx, *req.State, &res.Diagnostics) + old := helper.StateFrom[DockerV0](ctx, *req.State, &res.Diagnostics) if res.Diagnostics.HasError() { return } diff --git a/pkg/resources/docker/schema.go b/pkg/resources/docker/schema.go index e273ed97..427d773e 100644 --- a/pkg/resources/docker/schema.go +++ b/pkg/resources/docker/schema.go @@ -11,16 +11,19 @@ import ( "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/booldefault" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Docker struct { attributes.Runtime Dockerfile types.String `tfsdk:"dockerfile"` + Buildx types.Bool `tfsdk:"buildx"` ContainerPort types.Int64 `tfsdk:"container_port"` ContainerPortTCP types.Int64 `tfsdk:"container_port_tcp"` EnableIPv6 types.Bool `tfsdk:"enable_ipv6"` @@ -59,6 +62,12 @@ var schemaDocker = schema.Schema{ Optional: true, MarkdownDescription: "The name of the Dockerfile to build", }, + "buildx": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Set to true to use buildx to build the Docker image", + }, "container_port": schema.Int64Attribute{ Optional: true, MarkdownDescription: "Set to custom HTTP port if your Docker container runs on custom port", @@ -107,6 +116,8 @@ var schemaDocker = schema.Schema{ }, "daemon_socket_mount": schema.BoolAttribute{ Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), MarkdownDescription: "Set to true to access the host Docker socket from inside your container", }, }), @@ -169,6 +180,8 @@ var schemaDockerV0 = schema.Schema{ }, "daemon_socket_mount": schema.BoolAttribute{ Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), MarkdownDescription: "Set to true to access the host Docker socket from inside your container", }, }), @@ -176,32 +189,51 @@ var schemaDockerV0 = schema.Schema{ } func (p *Docker) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(p.Environment.ElementsAs(ctx, &customEnv, false)...) + // Start with common runtime environment variables (APP_FOLDER, Hooks, Environment) + env := p.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) - pkg.IfIsSetStr(p.AppFolder, func(s string) { env["APP_FOLDER"] = s }) + // Add Docker-specific environment variables + pkg.IfIsSetStr(p.Dockerfile, func(s string) { env[CC_DOCKERFILE] = s }) + pkg.IfIsSetB(p.Buildx, func(b bool) { env[CC_DOCKER_BUILDX] = strconv.FormatBool(b) }) + pkg.IfIsSetI(p.ContainerPort, func(i int64) { env[CC_DOCKER_EXPOSED_HTTP_PORT] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetI(p.ContainerPortTCP, func(i int64) { env[CC_DOCKER_EXPOSED_TCP_PORT] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(p.IPv6Cidr, func(s string) { env[CC_DOCKER_FIXED_CIDR_V6] = s }) + pkg.IfIsSetStr(p.RegistryURL, func(s string) { env[CC_DOCKER_LOGIN_SERVER] = s }) + pkg.IfIsSetStr(p.RegistryUser, func(s string) { env[CC_DOCKER_LOGIN_USERNAME] = s }) + pkg.IfIsSetStr(p.RegistryPassword, func(s string) { env[CC_DOCKER_LOGIN_PASSWORD] = s }) + pkg.IfIsSetB(p.DaemonSocketMount, func(e bool) { env[CC_MOUNT_DOCKER_SOCKET] = strconv.FormatBool(e) }) - // Docker specific - pkg.IfIsSetStr(p.Dockerfile, func(s string) { env["CC_DOCKERFILE"] = s }) - pkg.IfIsSetI(p.ContainerPort, func(i int64) { env["CC_DOCKER_EXPOSED_HTTP_PORT"] = fmt.Sprintf("%d", i) }) - pkg.IfIsSetI(p.ContainerPortTCP, func(i int64) { env["CC_DOCKER_EXPOSED_TCP_PORT"] = fmt.Sprintf("%d", i) }) - pkg.IfIsSetStr(p.IPv6Cidr, func(s string) { env["CC_DOCKER_FIXED_CIDR_V6"] = s }) - pkg.IfIsSetStr(p.RegistryURL, func(s string) { env["CC_DOCKER_LOGIN_SERVER"] = s }) - pkg.IfIsSetStr(p.RegistryUser, func(s string) { env["CC_DOCKER_LOGIN_USERNAME"] = s }) - pkg.IfIsSetStr(p.RegistryPassword, func(s string) { env["CC_DOCKER_LOGIN_PASSWORD"] = s }) - pkg.IfIsSetB(p.DaemonSocketMount, func(e bool) { env["CC_MOUNT_DOCKER_SOCKET"] = strconv.FormatBool(e) }) + return env +} - env = pkg.Merge(env, p.Hooks.ToEnv()) +// fromEnv iter on environment set on the clever application and +// handle language specific env vars +// put the others on Environment field +func (d *Docker) fromEnv(ctx context.Context, env map[string]string) diag.Diagnostics { + diags := diag.Diagnostics{} + m := helper.NewEnvMap(env) - return env + d.Dockerfile = pkg.FromStr(m.Pop(CC_DOCKERFILE)) + d.Buildx = pkg.FromBool(m.Pop(CC_DOCKER_BUILDX) == "true") + + if port, err := strconv.ParseInt(m.Pop(CC_DOCKER_EXPOSED_HTTP_PORT), 10, 64); err == nil { + d.ContainerPort = pkg.FromI(port) + } + if port, err := strconv.ParseInt(m.Pop(CC_DOCKER_EXPOSED_TCP_PORT), 10, 64); err == nil { + d.ContainerPortTCP = pkg.FromI(port) + } + + d.IPv6Cidr = pkg.FromStr(m.Pop(CC_DOCKER_FIXED_CIDR_V6)) + d.RegistryURL = pkg.FromStr(m.Pop(CC_DOCKER_LOGIN_SERVER)) + d.RegistryUser = pkg.FromStr(m.Pop(CC_DOCKER_LOGIN_USERNAME)) + d.RegistryPassword = pkg.FromStr(m.Pop(CC_DOCKER_LOGIN_PASSWORD)) + d.DaemonSocketMount = pkg.FromBool(m.Pop(CC_MOUNT_DOCKER_SOCKET) == "true") + + d.FromEnvironment(ctx, m) + return diags } func (p *Docker) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { diff --git a/pkg/resources/frankenphp/crud.go b/pkg/resources/frankenphp/crud.go index 8efbd18a..049020d0 100644 --- a/pkg/resources/frankenphp/crud.go +++ b/pkg/resources/frankenphp/crud.go @@ -50,6 +50,7 @@ func (r *ResourceFrankenPHP) Create(ctx context.Context, req resource.CreateRequ InstanceType: instance.Type, InstanceVariant: instance.Variant.ID, InstanceVersion: instance.Version, + BuildFlavor: plan.BuildFlavor.ValueString(), MinFlavor: plan.SmallestFlavor.ValueString(), MaxFlavor: plan.BiggestFlavor.ValueString(), MinInstances: plan.MinInstanceCount.ValueInt64(), @@ -100,13 +101,14 @@ func (r *ResourceFrankenPHP) Read(ctx context.Context, req resource.ReadRequest, state.Name = pkg.FromStr(appFrankenPHP.App.Name) state.Description = pkg.FromStr(appFrankenPHP.App.Description) + state.BuildFlavor = appFrankenPHP.GetBuildFlavor() state.MinInstanceCount = pkg.FromI(int64(appFrankenPHP.App.Instance.MinInstances)) state.MaxInstanceCount = pkg.FromI(int64(appFrankenPHP.App.Instance.MaxInstances)) state.SmallestFlavor = pkg.FromStr(appFrankenPHP.App.Instance.MinFlavor.Name) state.BiggestFlavor = pkg.FromStr(appFrankenPHP.App.Instance.MaxFlavor.Name) state.Region = pkg.FromStr(appFrankenPHP.App.Zone) state.DeployURL = pkg.FromStr(appFrankenPHP.App.DeployURL) - + state.fromEnv(ctx, appFrankenPHP.EnvAsMap()) state.VHosts = helper.VHostsFromAPIHosts(ctx, appFrankenPHP.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) @@ -154,6 +156,7 @@ func (r *ResourceFrankenPHP) Update(ctx context.Context, req resource.UpdateRequ InstanceType: instance.Type, InstanceVariant: instance.Variant.ID, InstanceVersion: instance.Version, + BuildFlavor: plan.BuildFlavor.ValueString(), MinFlavor: plan.SmallestFlavor.ValueString(), MaxFlavor: plan.BiggestFlavor.ValueString(), MinInstances: plan.MinInstanceCount.ValueInt64(), @@ -193,7 +196,14 @@ func (r *ResourceFrankenPHP) Delete(ctx context.Context, req resource.DeleteRequ } deleteAppRes := tmp.DeleteApp(ctx, r.Client(), r.Organization(), state.ID.ValueString()) + if deleteAppRes.IsNotFoundError() { + resp.State.RemoveResource(ctx) + return + } if deleteAppRes.HasError() { resp.Diagnostics.AddError("failed to delete app", deleteAppRes.Error().Error()) + return } + + resp.State.RemoveResource(ctx) } diff --git a/pkg/resources/frankenphp/frankenphp.go b/pkg/resources/frankenphp/frankenphp.go index 5581ba0f..6c3efb46 100644 --- a/pkg/resources/frankenphp/frankenphp.go +++ b/pkg/resources/frankenphp/frankenphp.go @@ -11,6 +11,14 @@ type ResourceFrankenPHP struct { helper.Configurer } +const ( + CC_FRANKENPHP_PORT = "CC_FRENKENPHP_PORT" + CC_FRANKENPHP_WORKER = "CC_FRENKENPHP_WORKER" + CC_PHP_COMPOSER_FLAGS = "CC_PHP_COMPOSER_FLAGS" + CC_PHP_DEV_DEPENDENCIES = "CC_PHP_DEV_DEPENDENCIES" + CC_WEBROOT = "CC_WEBROOT" +) + func NewResourceFrankenPHP() resource.Resource { return &ResourceFrankenPHP{} } diff --git a/pkg/resources/frankenphp/schema.go b/pkg/resources/frankenphp/schema.go index 26f7aefd..5eb1423c 100644 --- a/pkg/resources/frankenphp/schema.go +++ b/pkg/resources/frankenphp/schema.go @@ -3,6 +3,7 @@ package frankenphp import ( "context" _ "embed" + "strconv" "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -13,11 +14,16 @@ import ( "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type FrankenPHP struct { attributes.Runtime - DevDependencies types.Bool `tfsdk:"dev_dependencies"` + ListenedPort types.Int64 `tfsdk:"listened_port"` + WorkerPath types.String `tfsdk:"worker_path"` + ComposerFlags types.String `tfsdk:"composer_flags"` + DevDependencies types.Bool `tfsdk:"dev_dependencies"` + Webroot types.String `tfsdk:"webroot"` } //go:embed doc.md @@ -28,39 +34,82 @@ func (r ResourceFrankenPHP) Schema(ctx context.Context, req resource.SchemaReque Version: 1, MarkdownDescription: frankenphpDoc, Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + "listened_port": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The port on which FrankenPHP listens for HTTP requests", + }, + "worker_path": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Path to the worker script, relative to the root of your project (e.g. /worker/scrip.php)", + }, + "composer_flags": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Flags to pass to Composer", + }, "dev_dependencies": schema.BoolAttribute{ Optional: true, Computed: true, MarkdownDescription: "Install development dependencies (Default: false)", Default: booldefault.StaticBool(false), }, + "webroot": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Path to the web content to serve, relative to the root of your application", + }, }), Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), } } func (fp *FrankenPHP) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(fp.Environment.ElementsAs(ctx, &customEnv, false)...) + // Start with common runtime environment variables (APP_FOLDER, Hooks, Environment) + env := fp.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) + + // Add FrankenPHP-specific environment variables + pkg.IfIsSetI(fp.ListenedPort, func(i int64) { env[CC_FRANKENPHP_PORT] = pkg.FromI(i).String() }) + pkg.IfIsSetStr(fp.WorkerPath, func(s string) { env[CC_FRANKENPHP_WORKER] = s }) + pkg.IfIsSetStr(fp.ComposerFlags, func(s string) { env[CC_PHP_COMPOSER_FLAGS] = s }) + pkg.IfIsSetStr(fp.Webroot, func(s string) { env[CC_WEBROOT] = s }) pkg.IfIsSetB(fp.DevDependencies, func(devDeps bool) { if devDeps { - env["CC_PHP_DEV_DEPENDENCIES"] = "install" + env[CC_PHP_DEV_DEPENDENCIES] = "install" } }) - env = pkg.Merge(env, fp.Hooks.ToEnv()) return env } +// fromEnv iter on environment set on the clever application and +// handle language specific env vars +// put the others on Environment field +func (fp *FrankenPHP) fromEnv(ctx context.Context, env map[string]string) diag.Diagnostics { + diags := diag.Diagnostics{} + m := helper.NewEnvMap(env) + + // Parse FrankenPHP-specific environment variables + if port := m.Pop(CC_FRANKENPHP_PORT); port != "" { + if parsed, err := strconv.ParseInt(port, 10, 64); err == nil { + fp.ListenedPort = pkg.FromI(parsed) + } + } + + fp.WorkerPath = pkg.FromStr(m.Pop(CC_FRANKENPHP_WORKER)) + fp.ComposerFlags = pkg.FromStr(m.Pop(CC_PHP_COMPOSER_FLAGS)) + fp.Webroot = pkg.FromStr(m.Pop(CC_WEBROOT)) + + if devDeps := m.Pop(CC_PHP_DEV_DEPENDENCIES); devDeps != "" { + fp.DevDependencies = pkg.FromBool(devDeps == "install") + } + + // Handle common runtime variables (APP_FOLDER, Hooks, remaining Environment) + fp.FromEnvironment(ctx, m) + return diags +} + func (fp *FrankenPHP) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if fp.Deployment == nil || fp.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/golang/crud.go b/pkg/resources/golang/crud.go index 927278e1..dd62438c 100644 --- a/pkg/resources/golang/crud.go +++ b/pkg/resources/golang/crud.go @@ -112,6 +112,7 @@ func (r *ResourceGo) Read(ctx context.Context, req resource.ReadRequest, res *re state.StickySessions = pkg.FromBool(appRes.App.StickySessions) state.RedirectHTTPS = pkg.FromBool(application.ToForceHTTPS(appRes.App.ForceHTTPS)) state.VHosts = helper.VHostsFromAPIHosts(ctx, appRes.App.Vhosts.AsString(), state.VHosts, &res.Diagnostics) + state.fromEnv(ctx, appRes.EnvAsMap()) diags = res.State.Set(ctx, state) res.Diagnostics.Append(diags...) diff --git a/pkg/resources/golang/go.go b/pkg/resources/golang/go.go index f04561d8..b6bd8587 100644 --- a/pkg/resources/golang/go.go +++ b/pkg/resources/golang/go.go @@ -10,6 +10,12 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +const ( + CC_GO_BUILD_TOOL = "CC_GO_BUILD_TOOL" + CC_GO_PKG = "CC_GO_PKG" + CC_GO_RUNDIR = "CC_GO_RUNDIR" +) + type ResourceGo struct { helper.Configurer } diff --git a/pkg/resources/golang/schema.go b/pkg/resources/golang/schema.go index c08f6eac..da1e0d4b 100644 --- a/pkg/resources/golang/schema.go +++ b/pkg/resources/golang/schema.go @@ -8,13 +8,18 @@ import ( "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/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Go struct { attributes.Runtime + GoBuildTool types.String `tfsdk:"go_build_tool"` + GoPkg types.String `tfsdk:"go_pkg"` + GoRunDir types.String `tfsdk:"go_rundir"` } type GoV0 struct { @@ -31,8 +36,24 @@ func (r ResourceGo) Schema(ctx context.Context, req resource.SchemaRequest, res var schemaGo = schema.Schema{ Version: 1, MarkdownDescription: goDoc, - Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{}), - Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), + Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + // CC_GO_BUILD_TOOL + "go_build_tool": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Available values: `gomod`, `gobuild`. Build and install your application (`goget` is deprecated)", + }, + // CC_GO_PKG + "go_pkg": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Tell the `CC_GO_BUILD_TOOL` which file contains the `main()` function (default: `main.go`)", + }, + // CC_GO_RUNDIR + "go_rundir": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Run the application from the specified path, relative to `$GOPATH/src/` (deprecated)", + }, + }), + Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), } var schemaGoV0 = schema.Schema{ @@ -43,23 +64,28 @@ var schemaGoV0 = schema.Schema{ } func (g Go) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(g.Environment.ElementsAs(ctx, &customEnv, false)...) + env := g.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) - pkg.IfIsSetStr(g.AppFolder, func(s string) { env["APP_FOLDER"] = s }) - env = pkg.Merge(env, g.Hooks.ToEnv()) + pkg.IfIsSetStr(g.GoBuildTool, func(s string) { env[CC_GO_BUILD_TOOL] = s }) + pkg.IfIsSetStr(g.GoPkg, func(s string) { env[CC_GO_PKG] = s }) + pkg.IfIsSetStr(g.GoRunDir, func(s string) { env[CC_GO_RUNDIR] = s }) return env } +func (g *Go) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + g.GoBuildTool = pkg.FromStr(m.Pop(CC_GO_BUILD_TOOL)) + g.GoPkg = pkg.FromStr(m.Pop(CC_GO_PKG)) + g.GoRunDir = pkg.FromStr(m.Pop(CC_GO_RUNDIR)) + + g.FromEnvironment(ctx, m) +} + func (g Go) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if g.Deployment == nil || g.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/java/crud.go b/pkg/resources/java/crud.go index 8ed2d561..9cd4d0e4 100644 --- a/pkg/resources/java/crud.go +++ b/pkg/resources/java/crud.go @@ -109,17 +109,7 @@ func (r *ResourceJava) Read(ctx context.Context, req resource.ReadRequest, resp state.BuildFlavor = readRes.GetBuildFlavor() state.VHosts = helper.VHostsFromAPIHosts(ctx, readRes.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) - - for envName, envValue := range readRes.EnvAsMap() { - switch envName { - case "APP_FOLDER": - state.AppFolder = pkg.FromStr(envValue) - case "CC_JAVA_VERSION": - state.JavaVersion = pkg.FromStr(envValue) - default: - //state.Environment. - } - } + state.fromEnv(ctx, readRes.EnvAsMap()) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } diff --git a/pkg/resources/java/java.go b/pkg/resources/java/java.go index 0befa027..c3129c8a 100644 --- a/pkg/resources/java/java.go +++ b/pkg/resources/java/java.go @@ -10,6 +10,23 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +const ( + CC_DISABLE_MAX_METASPACE = "CC_DISABLE_MAX_METASPACE" + CC_EXTRA_JAVA_ARGS = "CC_EXTRA_JAVA_ARGS" + CC_JAR_ARGS = "CC_JAR_ARGS" + CC_JAR_PATH = "CC_JAR_PATH" + CC_JAVA_VERSION = "CC_JAVA_VERSION" + CC_MAVEN_PROFILES = "CC_MAVEN_PROFILES" + CC_RUN_COMMAND = "CC_RUN_COMMAND" + CC_SBT_TARGET_BIN = "CC_SBT_TARGET_BIN" + CC_SBT_TARGET_DIR = "CC_SBT_TARGET_DIR" + GRADLE_DEPLOY_GOAL = "GRADLE_DEPLOY_GOAL" + MAVEN_DEPLOY_GOAL = "MAVEN_DEPLOY_GOAL" + NUDGE_APPID = "NUDGE_APPID" + PLAY1_VERSION = "PLAY1_VERSION" + SBT_DEPLOY_GOAL = "SBT_DEPLOY_GOAL" +) + type ResourceJava struct { helper.Configurer profile string diff --git a/pkg/resources/java/schema.go b/pkg/resources/java/schema.go index 52112108..00dcdae7 100644 --- a/pkg/resources/java/schema.go +++ b/pkg/resources/java/schema.go @@ -3,22 +3,36 @@ package java import ( "context" _ "embed" - - "maps" + "strconv" "github.com/go-git/go-git/v5/plumbing/transport/http" "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/booldefault" "github.com/hashicorp/terraform-plugin-framework/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Java struct { attributes.Runtime - JavaVersion types.String `tfsdk:"java_version"` + DisableMaxMetaspace types.Bool `tfsdk:"disable_max_metaspace"` + ExtraJavaArgs types.String `tfsdk:"extra_java_args"` + GradleDeployGoal types.String `tfsdk:"gradle_deploy_goal"` + JarArgs types.String `tfsdk:"jar_args"` + JarPath types.String `tfsdk:"jar_path"` + JavaVersion types.String `tfsdk:"java_version"` + MavenDeployGoal types.String `tfsdk:"maven_deploy_goal"` + MavenProfiles types.String `tfsdk:"maven_profiles"` + NudgeAppId types.String `tfsdk:"nudge_app_id"` + Play1Version types.String `tfsdk:"play1_version"` + RunCommand types.String `tfsdk:"run_command"` + SbtDeployGoal types.String `tfsdk:"sbt_deploy_goal"` + SbtTargetBin types.String `tfsdk:"sbt_target_bin"` + SbtTargetDir types.String `tfsdk:"sbt_target_dir"` } type JavaV0 struct { @@ -37,9 +51,77 @@ var schemaJava = schema.Schema{ Version: 1, MarkdownDescription: javaDoc, Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + // CC_DISABLE_MAX_METASPACE + "disable_max_metaspace": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Allows to disable the Java option `-XX:MaxMetaspaceSize`", + }, + // CC_EXTRA_JAVA_ARGS + "extra_java_args": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define extra arguments to pass to `java` for JAR", + }, + // GRADLE_DEPLOY_GOAL + "gradle_deploy_goal": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which Gradle goals to run during build", + }, + // CC_JAR_ARGS + "jar_args": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define arguments to pass to the launched JAR", + }, + // CC_JAR_PATH + "jar_path": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define the path to your JAR", + }, + // CC_JAVA_VERSION "java_version": schema.StringAttribute{ - Optional: true, - Description: "Choose the JVM version between 7 to 24 for OpenJDK or graalvm-ce for GraalVM 21.0.0.2 (based on OpenJDK 11.0).", + Optional: true, + MarkdownDescription: "Choose the JVM version between 7 to 24 for OpenJDK or `graalvm-ce` for GraalVM (default: 21)", + }, + // MAVEN_DEPLOY_GOAL + "maven_deploy_goal": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which Maven goals to run during build", + }, + // CC_MAVEN_PROFILES + "maven_profiles": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which Maven profile to use during default build", + }, + // NUDGE_APPID + "nudge_app_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Nudge application ID", + }, + // PLAY1_VERSION + "play1_version": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which play1 version to use between `1.2`, `1.3`, `1.4` and `1.5`", + }, + // CC_RUN_COMMAND + "run_command": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Custom command to run your application. Replaces the default behavior", + }, + // SBT_DEPLOY_GOAL + "sbt_deploy_goal": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which SBT goals to run during build (default: `stage`)", + }, + // CC_SBT_TARGET_BIN + "sbt_target_bin": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define the bin to pick in the `CC_SBT_TARGET_DIR`", + }, + // CC_SBT_TARGET_DIR + "sbt_target_dir": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define the folder the `target` dir is in (default: `.`)", }, }), Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), @@ -58,22 +140,52 @@ var schemaJavaV0 = schema.Schema{ } func (plan *Java) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(plan.Environment.ElementsAs(ctx, &customEnv, false)...) + env := plan.ToEnv(ctx, diags) if diags.HasError() { return env } - maps.Copy(env, customEnv) - pkg.IfIsSetStr(plan.AppFolder, func(s string) { env["APP_FOLDER"] = s }) - pkg.IfIsSetStr(plan.JavaVersion, func(s string) { env["CC_JAVA_VERSION"] = s }) + pkg.IfIsSetB(plan.DisableMaxMetaspace, func(b bool) { env[CC_DISABLE_MAX_METASPACE] = strconv.FormatBool(b) }) + pkg.IfIsSetStr(plan.ExtraJavaArgs, func(s string) { env[CC_EXTRA_JAVA_ARGS] = s }) + pkg.IfIsSetStr(plan.GradleDeployGoal, func(s string) { env[GRADLE_DEPLOY_GOAL] = s }) + pkg.IfIsSetStr(plan.JarArgs, func(s string) { env[CC_JAR_ARGS] = s }) + pkg.IfIsSetStr(plan.JarPath, func(s string) { env[CC_JAR_PATH] = s }) + pkg.IfIsSetStr(plan.JavaVersion, func(s string) { env[CC_JAVA_VERSION] = s }) + pkg.IfIsSetStr(plan.MavenDeployGoal, func(s string) { env[MAVEN_DEPLOY_GOAL] = s }) + pkg.IfIsSetStr(plan.MavenProfiles, func(s string) { env[CC_MAVEN_PROFILES] = s }) + pkg.IfIsSetStr(plan.NudgeAppId, func(s string) { env[NUDGE_APPID] = s }) + pkg.IfIsSetStr(plan.Play1Version, func(s string) { env[PLAY1_VERSION] = s }) + pkg.IfIsSetStr(plan.RunCommand, func(s string) { env[CC_RUN_COMMAND] = s }) + pkg.IfIsSetStr(plan.SbtDeployGoal, func(s string) { env[SBT_DEPLOY_GOAL] = s }) + pkg.IfIsSetStr(plan.SbtTargetBin, func(s string) { env[CC_SBT_TARGET_BIN] = s }) + pkg.IfIsSetStr(plan.SbtTargetDir, func(s string) { env[CC_SBT_TARGET_DIR] = s }) + return env } +func (java *Java) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + if disable, err := strconv.ParseBool(m.Pop(CC_DISABLE_MAX_METASPACE)); err == nil { + java.DisableMaxMetaspace = pkg.FromBool(disable) + } + java.ExtraJavaArgs = pkg.FromStr(m.Pop(CC_EXTRA_JAVA_ARGS)) + java.GradleDeployGoal = pkg.FromStr(m.Pop(GRADLE_DEPLOY_GOAL)) + java.JarArgs = pkg.FromStr(m.Pop(CC_JAR_ARGS)) + java.JarPath = pkg.FromStr(m.Pop(CC_JAR_PATH)) + java.JavaVersion = pkg.FromStr(m.Pop(CC_JAVA_VERSION)) + java.MavenDeployGoal = pkg.FromStr(m.Pop(MAVEN_DEPLOY_GOAL)) + java.MavenProfiles = pkg.FromStr(m.Pop(CC_MAVEN_PROFILES)) + java.NudgeAppId = pkg.FromStr(m.Pop(NUDGE_APPID)) + java.Play1Version = pkg.FromStr(m.Pop(PLAY1_VERSION)) + java.RunCommand = pkg.FromStr(m.Pop(CC_RUN_COMMAND)) + java.SbtDeployGoal = pkg.FromStr(m.Pop(SBT_DEPLOY_GOAL)) + java.SbtTargetBin = pkg.FromStr(m.Pop(CC_SBT_TARGET_BIN)) + java.SbtTargetDir = pkg.FromStr(m.Pop(CC_SBT_TARGET_DIR)) + + java.FromEnvironment(ctx, m) +} + func (java *Java) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if java.Deployment == nil || java.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/keycloak/keycloak_test.go b/pkg/resources/keycloak/keycloak_test.go index 1bf8fae5..7ed9b3ce 100644 --- a/pkg/resources/keycloak/keycloak_test.go +++ b/pkg/resources/keycloak/keycloak_test.go @@ -30,7 +30,7 @@ func TestAccKeycloak_basic(t *testing.T) { materiakvBlock := helper.NewRessource( "clevercloud_keycloak", rName, - helper.SetKeyValues(map[string]any{"name": rName, "plan": "base", "region": "par"}), + helper.SetKeyValues(map[string]any{"name": rName, "region": "par"}), ) resource.Test(t, resource.TestCase{ diff --git a/pkg/resources/metabase/metabase_test.go b/pkg/resources/metabase/metabase_test.go index c25b31e6..1cd47f95 100644 --- a/pkg/resources/metabase/metabase_test.go +++ b/pkg/resources/metabase/metabase_test.go @@ -31,7 +31,7 @@ func TestAccMetabase_basic(t *testing.T) { metabaseBlock := helper.NewRessource( "clevercloud_metabase", rName, - helper.SetKeyValues(map[string]any{"name": rName, "plan": "base", "region": "par"}), + helper.SetKeyValues(map[string]any{"name": rName, "region": "par"}), ) resource.Test(t, resource.TestCase{ diff --git a/pkg/resources/mongodb/crud.go b/pkg/resources/mongodb/crud.go index 5e8becda..286f9bd7 100644 --- a/pkg/resources/mongodb/crud.go +++ b/pkg/resources/mongodb/crud.go @@ -42,14 +42,15 @@ func (r *ResourceMongoDB) Create(ctx context.Context, req resource.CreateRequest resp.Diagnostics.AddError("failed to create addon", res.Error().Error()) return } + addon := res.Payload() - mg.ID = pkg.FromStr(res.Payload().RealID) - mg.CreationDate = pkg.FromI(res.Payload().CreationDate) - mg.Plan = pkg.FromStr(res.Payload().Plan.Slug) + mg.ID = pkg.FromStr(addon.RealID) + mg.CreationDate = pkg.FromI(addon.CreationDate) + mg.Plan = pkg.FromStr(strings.ToLower(addon.Plan.Slug)) resp.Diagnostics.Append(resp.State.Set(ctx, mg)...) - mgInfoRes := tmp.GetMongoDB(ctx, r.Client(), res.Payload().ID) + mgInfoRes := tmp.GetMongoDB(ctx, r.Client(), addon.ID) if mgInfoRes.HasError() { resp.Diagnostics.AddError("failed to get MongoDB connection infos", mgInfoRes.Error().Error()) return diff --git a/pkg/resources/mysql/crud.go b/pkg/resources/mysql/crud.go index fbeadd9d..aca139e0 100644 --- a/pkg/resources/mysql/crud.go +++ b/pkg/resources/mysql/crud.go @@ -89,7 +89,7 @@ func (r *ResourceMySQL) Create(ctx context.Context, req resource.CreateRequest, my.ID = pkg.FromStr(createdMy.RealID) my.CreationDate = pkg.FromI(createdMy.CreationDate) - my.Plan = pkg.FromStr(createdMy.Plan.Slug) + my.Plan = pkg.FromStr(strings.ToLower(createdMy.Plan.Slug)) resp.Diagnostics.Append(resp.State.Set(ctx, my)...) @@ -165,7 +165,7 @@ func (r *ResourceMySQL) Read(ctx context.Context, req resource.ReadRequest, resp tflog.Debug(ctx, "API", map[string]any{"my": addonMy}) my.ID = pkg.FromStr(addon.RealID) my.Name = pkg.FromStr(addon.Name) - my.Plan = pkg.FromStr(addonMy.Plan) + my.Plan = pkg.FromStr(strings.ToLower(addonMy.Plan)) my.Region = pkg.FromStr(addonMy.Zone) my.CreationDate = pkg.FromI(addon.CreationDate) my.Host = pkg.FromStr(addonMy.Host) diff --git a/pkg/resources/nodejs/crud.go b/pkg/resources/nodejs/crud.go index 3c98cb9c..f83b82c0 100644 --- a/pkg/resources/nodejs/crud.go +++ b/pkg/resources/nodejs/crud.go @@ -108,6 +108,8 @@ func (r *ResourceNodeJS) Read(ctx context.Context, req resource.ReadRequest, res state.SmallestFlavor = pkg.FromStr(appRes.App.Instance.MinFlavor.Name) state.BiggestFlavor = pkg.FromStr(appRes.App.Instance.MaxFlavor.Name) + resp.Diagnostics.Append(state.fromEnv(ctx, appRes.EnvAsMap())...) + state.VHosts = helper.VHostsFromAPIHosts(ctx, appRes.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) diags = resp.State.Set(ctx, state) diff --git a/pkg/resources/nodejs/nodejs.go b/pkg/resources/nodejs/nodejs.go index cfe422c5..2a9cba72 100644 --- a/pkg/resources/nodejs/nodejs.go +++ b/pkg/resources/nodejs/nodejs.go @@ -14,6 +14,17 @@ type ResourceNodeJS struct { helper.Configurer } +const ( + CC_NODE_VERSION = "CC_NODE_VERSION" + CC_NODE_DEV_DEPENDENCIES = "CC_NODE_DEV_DEPENDENCIES" + CC_RUN_COMMAND = "CC_RUN_COMMAND" + CC_NODE_BUILD_TOOL = "CC_NODE_BUILD_TOOL" + CC_CUSTOM_BUILD_TOOL = "CC_CUSTOM_BUILD_TOOL" + CC_NPM_REGISTRY = "CC_NPM_REGISTRY" + CC_NPM_BASIC_AUTH = "CC_NPM_BASIC_AUTH" + NPM_TOKEN = "NPM_TOKEN" +) + func NewResourceNodeJS() resource.Resource { return &ResourceNodeJS{} } diff --git a/pkg/resources/nodejs/schema.go b/pkg/resources/nodejs/schema.go index 024bafd7..ccc5d3e8 100644 --- a/pkg/resources/nodejs/schema.go +++ b/pkg/resources/nodejs/schema.go @@ -12,15 +12,19 @@ import ( "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type NodeJS struct { attributes.Runtime - DevDependencies types.Bool `tfsdk:"dev_dependencies"` - StartScript types.String `tfsdk:"start_script"` - PackageManager types.String `tfsdk:"package_manager"` - Registry types.String `tfsdk:"registry"` - RegistryToken types.String `tfsdk:"registry_token"` + NodeVersion types.String `tfsdk:"node_version"` + DevDependencies types.Bool `tfsdk:"dev_dependencies"` + StartScript types.String `tfsdk:"start_script"` + PackageManager types.String `tfsdk:"package_manager"` + CustomBuildTool types.String `tfsdk:"custom_build_tool"` + Registry types.String `tfsdk:"registry"` + RegistryBasicAuth types.String `tfsdk:"registry_basic_auth"` + RegistryToken types.String `tfsdk:"registry_token"` } type NodeJSV0 struct { @@ -43,6 +47,11 @@ var schemaNodeJS = schema.Schema{ Version: 1, MarkdownDescription: nodejsDoc, Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + // CC_NODE_VERSION + "node_version": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Set Node.js version, for example `24`, `23.11` or `22.15.1`", + }, // CC_NODE_DEV_DEPENDENCIES "dev_dependencies": schema.BoolAttribute{ Optional: true, @@ -53,15 +62,26 @@ var schemaNodeJS = schema.Schema{ Optional: true, MarkdownDescription: "Set custom start script, instead of `npm start`", }, - // CC_NODE_BUILD_TOOL / CC_CUSTOM_BUILD_TOOL + // CC_NODE_BUILD_TOOL "package_manager": schema.StringAttribute{ Optional: true, - MarkdownDescription: "Either npm, npm-ci, bun, pnpm, yarn-berry or custom", + MarkdownDescription: "Choose your build tool between npm, npm-ci, yarn, yarn2 and custom. Default is `npm`", + }, + // CC_CUSTOM_BUILD_TOOL + "custom_build_tool": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "A custom command to run (with package_manager set to `custom`)", }, // CC_NPM_REGISTRY "registry": schema.StringAttribute{ Optional: true, - MarkdownDescription: "The host of your private repository, available values: github or the registry host", + MarkdownDescription: "The host of your private repository, available values: github or the registry host. Default is `registry.npmjs.org`", + }, + // CC_NPM_BASIC_AUTH + "registry_basic_auth": schema.StringAttribute{ + Optional: true, + Sensitive: true, + MarkdownDescription: "Private repository credentials, in the form `user:password`. You can't use this if registry_token is set", }, // NPM_TOKEN "registry_token": schema.StringAttribute{ @@ -108,28 +128,55 @@ var schemaNodeJSV0 = schema.Schema{ } func (node NodeJS) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(node.Environment.ElementsAs(ctx, &customEnv, false)...) + // Start with common runtime environment variables (APP_FOLDER, Hooks, Environment) + env := node.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) - pkg.IfIsSetStr(node.AppFolder, func(s string) { env["APP_FOLDER"] = s }) - pkg.IfIsSetB(node.DevDependencies, func(s bool) { env["CC_NODE_DEV_DEPENDENCIES"] = "install" }) - pkg.IfIsSetStr(node.StartScript, func(s string) { env["CC_RUN_COMMAND"] = s }) - pkg.IfIsSetStr(node.PackageManager, func(s string) { env["CC_NODE_BUILD_TOOL"] = s }) - pkg.IfIsSetStr(node.Registry, func(s string) { env["CC_NPM_REGISTRY"] = s }) - pkg.IfIsSetStr(node.RegistryToken, func(s string) { env["NPM_TOKEN"] = s }) - env = pkg.Merge(env, node.Hooks.ToEnv()) + // Add Node.js-specific environment variables + pkg.IfIsSetStr(node.NodeVersion, func(s string) { env[CC_NODE_VERSION] = s }) + pkg.IfIsSetB(node.DevDependencies, func(b bool) { + if b { + env[CC_NODE_DEV_DEPENDENCIES] = "install" + } + }) + pkg.IfIsSetStr(node.StartScript, func(s string) { env[CC_RUN_COMMAND] = s }) + pkg.IfIsSetStr(node.PackageManager, func(s string) { env[CC_NODE_BUILD_TOOL] = s }) + pkg.IfIsSetStr(node.CustomBuildTool, func(s string) { env[CC_CUSTOM_BUILD_TOOL] = s }) + pkg.IfIsSetStr(node.Registry, func(s string) { env[CC_NPM_REGISTRY] = s }) + pkg.IfIsSetStr(node.RegistryBasicAuth, func(s string) { env[CC_NPM_BASIC_AUTH] = s }) + pkg.IfIsSetStr(node.RegistryToken, func(s string) { env[NPM_TOKEN] = s }) return env } +// fromEnv iter on environment set on the clever application and +// handle language specific env vars +// put the others on Environment field +func (node *NodeJS) fromEnv(ctx context.Context, env map[string]string) diag.Diagnostics { + diags := diag.Diagnostics{} + m := helper.NewEnvMap(env) + + // Parse Node.js-specific environment variables + node.NodeVersion = pkg.FromStr(m.Pop(CC_NODE_VERSION)) + + if devDeps := m.Pop(CC_NODE_DEV_DEPENDENCIES); devDeps != "" { + node.DevDependencies = pkg.FromBool(devDeps == "install") + } + + node.StartScript = pkg.FromStr(m.Pop(CC_RUN_COMMAND)) + node.PackageManager = pkg.FromStr(m.Pop(CC_NODE_BUILD_TOOL)) + node.CustomBuildTool = pkg.FromStr(m.Pop(CC_CUSTOM_BUILD_TOOL)) + node.Registry = pkg.FromStr(m.Pop(CC_NPM_REGISTRY)) + node.RegistryBasicAuth = pkg.FromStr(m.Pop(CC_NPM_BASIC_AUTH)) + node.RegistryToken = pkg.FromStr(m.Pop(NPM_TOKEN)) + + // Handle common runtime variables (APP_FOLDER, Hooks, remaining Environment) + node.FromEnvironment(ctx, m) + return diags +} + func (node NodeJS) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if node.Deployment == nil || node.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/php/crud.go b/pkg/resources/php/crud.go index ee5b6a9f..c301713e 100644 --- a/pkg/resources/php/crud.go +++ b/pkg/resources/php/crud.go @@ -109,6 +109,8 @@ func (r *ResourcePHP) Read(ctx context.Context, req resource.ReadRequest, resp * state.DeployURL = pkg.FromStr(appPHP.App.DeployURL) state.BuildFlavor = appPHP.GetBuildFlavor() + state.fromEnv(ctx, appPHP.EnvAsMap()) + state.VHosts = helper.VHostsFromAPIHosts(ctx, appPHP.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) diff --git a/pkg/resources/php/php.go b/pkg/resources/php/php.go index 52800c19..3fccffe0 100644 --- a/pkg/resources/php/php.go +++ b/pkg/resources/php/php.go @@ -3,6 +3,7 @@ package php import ( "context" + "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/attributes" "go.clever-cloud.com/terraform-provider/pkg/helper" @@ -14,6 +15,43 @@ type ResourcePHP struct { helper.Configurer } +const ( + ALWAYS_POPULATE_RAW_POST_DATA = "ALWAYS_POPULATE_RAW_POST_DATA" + CC_COMPOSER_VERSION = "CC_COMPOSER_VERSION" + CC_CGI_IMPLEMENTATION = "CC_CGI_IMPLEMENTATION" + CC_HTTP_BASIC_AUTH = "CC_HTTP_BASIC_AUTH" + CC_APACHE_HEADERS_SIZE = "CC_APACHE_HEADERS_SIZE" + CC_LDAP_CA_CERT = "CC_LDAP_CA_CERT" + CC_MTA_AUTH_PASSWORD = "CC_MTA_AUTH_PASSWORD" + CC_MTA_AUTH_USER = "CC_MTA_AUTH_USER" + CC_MTA_SERVER_AUTH_METHOD = "CC_MTA_SERVER_AUTH_METHOD" + CC_MTA_SERVER_HOST = "CC_MTA_SERVER_HOST" + CC_MTA_SERVER_PORT = "CC_MTA_SERVER_PORT" + CC_MTA_SERVER_USE_TLS = "CC_MTA_SERVER_USE_TLS" + CC_OPCACHE_INTERNED_STRINGS_BUFFER = "CC_OPCACHE_INTERNED_STRINGS_BUFFER" + CC_OPCACHE_MAX_ACCELERATED_FILES = "CC_OPCACHE_MAX_ACCELERATED_FILES" + CC_OPCACHE_MEMORY = "CC_OPCACHE_MEMORY" + CC_OPCACHE_PRELOAD = "CC_OPCACHE_PRELOAD" + CC_PHP_ASYNC_APP_BUCKET = "CC_PHP_ASYNC_APP_BUCKET" + CC_PHP_DEV_DEPENDENCIES = "CC_PHP_DEV_DEPENDENCIES" + CC_PHP_DISABLE_APP_BUCKET = "CC_PHP_DISABLE_APP_BUCKET" + CC_PHP_VERSION = "CC_PHP_VERSION" + CC_REALPATH_CACHE_TTL = "CC_REALPATH_CACHE_TTL" + CC_WEBROOT = "CC_WEBROOT" + ENABLE_ELASTIC_APM_AGENT = "ENABLE_ELASTIC_APM_AGENT" + ENABLE_GRPC = "ENABLE_GRPC" + ENABLE_PDFLIB = "ENABLE_PDFLIB" + ENABLE_REDIS = "ENABLE_REDIS" + HTTP_TIMEOUT = "HTTP_TIMEOUT" + LDAPTLS_CACERT = "LDAPTLS_CACERT" + MAX_INPUT_VARS = "MAX_INPUT_VARS" + MEMORY_LIMIT = "MEMORY_LIMIT" + SESSION_TYPE = "SESSION_TYPE" + SOCKSIFY_EVERYTHING = "SOCKSIFY_EVERYTHING" + SQREEN_API_APP_NAME = "SQREEN_API_APP_NAME" + SQREEN_API_TOKEN = "SQREEN_API_TOKEN" +) + func NewResourcePHP() resource.Resource { return &ResourcePHP{} } @@ -60,10 +98,22 @@ func (r *ResourcePHP) UpgradeState(ctx context.Context) map[int64]resource.State AppFolder: old.AppFolder, Environment: old.Environment, }, - PHPVersion: old.PHPVersion, - WebRoot: old.WebRoot, - RedisSessions: old.RedisSessions, - DevDependencies: old.DevDependencies, + PHPVersion: old.PHPVersion, + WebRoot: old.WebRoot, + } + + // Migrate RedisSessions (Bool) to SessionType (String) + if !old.RedisSessions.IsNull() && old.RedisSessions.ValueBool() { + newState.SessionType = pkg.FromStr("redis") + } + + // Migrate DevDependencies from Bool to String + if !old.DevDependencies.IsNull() { + if old.DevDependencies.ValueBool() { + newState.DevDependencies = pkg.FromStr("install") + } else { + newState.DevDependencies = pkg.FromStr("ignore") + } } res.Diagnostics.Append(res.State.Set(ctx, newState)...) diff --git a/pkg/resources/php/schema.go b/pkg/resources/php/schema.go index 5c833a71..0a070355 100644 --- a/pkg/resources/php/schema.go +++ b/pkg/resources/php/schema.go @@ -3,23 +3,57 @@ package php import ( "context" _ "embed" + "fmt" + "strconv" "github.com/go-git/go-git/v5/plumbing/transport/http" "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/booldefault" "github.com/hashicorp/terraform-plugin-framework/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type PHP struct { attributes.Runtime - PHPVersion types.String `tfsdk:"php_version"` - WebRoot types.String `tfsdk:"webroot"` - RedisSessions types.Bool `tfsdk:"redis_sessions"` - DevDependencies types.Bool `tfsdk:"dev_dependencies"` + AlwaysPopulateRawPostData types.String `tfsdk:"always_populate_raw_post_data"` + ComposerVersion types.String `tfsdk:"composer_version"` + CgiImplementation types.String `tfsdk:"cgi_implementation"` + HttpBasicAuth types.String `tfsdk:"http_basic_auth"` + ApacheHeadersSize types.Int64 `tfsdk:"apache_headers_size"` + LdapCaCert types.String `tfsdk:"ldap_ca_cert"` + MtaAuthPassword types.String `tfsdk:"mta_auth_password"` + MtaAuthUser types.String `tfsdk:"mta_auth_user"` + MtaServerAuthMethod types.String `tfsdk:"mta_server_auth_method"` + MtaServerHost types.String `tfsdk:"mta_server_host"` + MtaServerPort types.Int64 `tfsdk:"mta_server_port"` + MtaServerUseTLS types.Bool `tfsdk:"mta_server_use_tls"` + OpcacheInternedStringsBuffer types.Int64 `tfsdk:"opcache_interned_strings_buffer"` + OpcacheMaxAcceleratedFiles types.Int64 `tfsdk:"opcache_max_accelerated_files"` + OpcacheMemory types.String `tfsdk:"opcache_memory"` + OpcachePreload types.String `tfsdk:"opcache_preload"` + AsyncAppBucket types.String `tfsdk:"async_app_bucket"` + DevDependencies types.String `tfsdk:"dev_dependencies"` + DisableAppBucket types.String `tfsdk:"disable_app_bucket"` + PHPVersion types.String `tfsdk:"php_version"` + RealpathCacheTTL types.Int64 `tfsdk:"realpath_cache_ttl"` + WebRoot types.String `tfsdk:"webroot"` + EnableElasticApmAgent types.Bool `tfsdk:"enable_elastic_apm_agent"` + EnableGrpc types.Bool `tfsdk:"enable_grpc"` + EnablePdflib types.Bool `tfsdk:"enable_pdflib"` + EnableRedis types.Bool `tfsdk:"enable_redis"` + HttpTimeout types.Int64 `tfsdk:"http_timeout"` + LdaptlsCacert types.String `tfsdk:"ldaptls_cacert"` + MaxInputVars types.Int64 `tfsdk:"max_input_vars"` + MemoryLimit types.String `tfsdk:"memory_limit"` + SessionType types.String `tfsdk:"session_type"` + SocksifyEverything types.Bool `tfsdk:"socksify_everything"` + SqreenApiAppName types.String `tfsdk:"sqreen_api_app_name"` + SqreenApiToken types.String `tfsdk:"sqreen_api_token"` } type PHPV0 struct { @@ -41,23 +75,156 @@ var schemaPHP = schema.Schema{ Version: 1, MarkdownDescription: phpDoc, Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ - // CC_WEBROOT + "always_populate_raw_post_data": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Controls population of raw POST data", + }, + "composer_version": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Choose your composer version between 1 and 2. Default is `2`", + }, + "cgi_implementation": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Choose the Apache FastCGI module between `fastcgi` and `proxy_fcgi`. Default is `proxy_fcgi`", + }, + "http_basic_auth": schema.StringAttribute{ + Optional: true, + Sensitive: true, + MarkdownDescription: "Restrict HTTP access to your application. Example: `login:password`. You can define multiple credentials using additional `CC_HTTP_BASIC_AUTH_n` (where `n` is a number) environment variables", + }, + "apache_headers_size": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Set the maximum size of the headers in Apache, between `8` and `256`. Default is `8`", + }, + "ldap_ca_cert": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Path to the LDAP CA certificate", + }, + "mta_auth_password": schema.StringAttribute{ + Optional: true, + Sensitive: true, + MarkdownDescription: "Password to authenticate to the SMTP server", + }, + "mta_auth_user": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "User to authenticate to the SMTP server", + }, + "mta_server_auth_method": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Enable or disable authentication to the SMTP server. Default is `on`", + }, + "mta_server_host": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Host of the SMTP server", + }, + "mta_server_port": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Port of the SMTP server. Default is `465`", + }, + "mta_server_use_tls": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Enable or disable TLS when connecting to the SMTP server. Default is `true`", + }, + "opcache_interned_strings_buffer": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The amount of memory used to store interned strings, in megabytes. Default is `4` (PHP5), `8` (PHP7)", + }, + "opcache_max_accelerated_files": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Maximum number of files handled by opcache. Default depends on the scaler size", + }, + "opcache_memory": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Set the shared opcache memory size. Default is about 1/8 of the RAM", + }, + "opcache_preload": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The path of the PHP preload file (PHP version 7.4 or higher)", + }, + "async_app_bucket": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Mount the default app FS bucket asynchronously. If set, should have value `async`", + }, + "dev_dependencies": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Control if development dependencies are installed or not. Values are either `install` or `ignore`", + }, + "disable_app_bucket": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Disable entirely the app FS Bucket. Values are either `true`, `yes` or `disable`", + }, "php_version": schema.StringAttribute{ Optional: true, - MarkdownDescription: "PHP version (Default: 8)", + MarkdownDescription: "Choose your PHP version among those supported. Default is `8.3`", + }, + "realpath_cache_ttl": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "The size of the realpath cache to be used by PHP. Default is `120`", }, "webroot": schema.StringAttribute{ Optional: true, - MarkdownDescription: "Define the DocumentRoot of your project (default: \".\")", + MarkdownDescription: "Define the DocumentRoot of your project. Default is `.`", }, - - "redis_sessions": schema.BoolAttribute{ + "enable_elastic_apm_agent": schema.BoolAttribute{ Optional: true, - MarkdownDescription: "Use a linked Redis instance to store sessions (Default: false)", + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Enable the Elastic APM Agent for PHP. Default is `true` if `ELASTIC_APM_SERVER_URL` is defined, `false` otherwise", }, - "dev_dependencies": schema.BoolAttribute{ + "enable_grpc": schema.BoolAttribute{ Optional: true, - MarkdownDescription: "Install development dependencies", + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Enable the use of gRPC module. Default is `false`", + }, + "enable_pdflib": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Enable the use of PDFlib module. Default is `false`", + }, + "enable_redis": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Enable Redis support. Default is `false`", + }, + "http_timeout": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Define a custom HTTP timeout. Default is `180`", + }, + "ldaptls_cacert": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Path to the LDAP TLS CA certificate", + }, + "max_input_vars": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Maximum number of input variables that can be accepted", + }, + "memory_limit": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Change the default memory limit for PHP scripts", + }, + "session_type": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Choose `redis` to use Redis as session store", + }, + "socksify_everything": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Enable SOCKS proxy for all outgoing connections. Default is `false`", + }, + "sqreen_api_app_name": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The name of your Sqreen application", + }, + "sqreen_api_token": schema.StringAttribute{ + Optional: true, + Sensitive: true, + MarkdownDescription: "Your Sqreen organization token", }, }), Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), @@ -90,35 +257,124 @@ var schemaPHPV0 = schema.Schema{ } func (p *PHP) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(p.Environment.ElementsAs(ctx, &customEnv, false)...) + // Start with common runtime environment variables (APP_FOLDER, Hooks, Environment) + env := p.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) - - pkg.IfIsSetStr(p.AppFolder, func(s string) { env["APP_FOLDER"] = s }) - pkg.IfIsSetStr(p.WebRoot, func(webroot string) { env["CC_WEBROOT"] = webroot }) - pkg.IfIsSetStr(p.PHPVersion, func(version string) { env["CC_PHP_VERSION"] = version }) - pkg.IfIsSetB(p.DevDependencies, func(devDeps bool) { - if devDeps { - env["CC_PHP_DEV_DEPENDENCIES"] = "install" - } - }) - pkg.IfIsSetB(p.RedisSessions, func(redis bool) { - if redis { - env["SESSION_TYPE"] = "redis" - } - }) - env = pkg.Merge(env, p.Hooks.ToEnv()) + + // Add PHP-specific environment variables + pkg.IfIsSetStr(p.AlwaysPopulateRawPostData, func(s string) { env[ALWAYS_POPULATE_RAW_POST_DATA] = s }) + pkg.IfIsSetStr(p.ComposerVersion, func(s string) { env[CC_COMPOSER_VERSION] = s }) + pkg.IfIsSetStr(p.CgiImplementation, func(s string) { env[CC_CGI_IMPLEMENTATION] = s }) + pkg.IfIsSetStr(p.HttpBasicAuth, func(s string) { env[CC_HTTP_BASIC_AUTH] = s }) + pkg.IfIsSetI(p.ApacheHeadersSize, func(i int64) { env[CC_APACHE_HEADERS_SIZE] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(p.LdapCaCert, func(s string) { env[CC_LDAP_CA_CERT] = s }) + pkg.IfIsSetStr(p.MtaAuthPassword, func(s string) { env[CC_MTA_AUTH_PASSWORD] = s }) + pkg.IfIsSetStr(p.MtaAuthUser, func(s string) { env[CC_MTA_AUTH_USER] = s }) + pkg.IfIsSetStr(p.MtaServerAuthMethod, func(s string) { env[CC_MTA_SERVER_AUTH_METHOD] = s }) + pkg.IfIsSetStr(p.MtaServerHost, func(s string) { env[CC_MTA_SERVER_HOST] = s }) + pkg.IfIsSetI(p.MtaServerPort, func(i int64) { env[CC_MTA_SERVER_PORT] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetB(p.MtaServerUseTLS, func(b bool) { env[CC_MTA_SERVER_USE_TLS] = strconv.FormatBool(b) }) + pkg.IfIsSetI(p.OpcacheInternedStringsBuffer, func(i int64) { env[CC_OPCACHE_INTERNED_STRINGS_BUFFER] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetI(p.OpcacheMaxAcceleratedFiles, func(i int64) { env[CC_OPCACHE_MAX_ACCELERATED_FILES] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(p.OpcacheMemory, func(s string) { env[CC_OPCACHE_MEMORY] = s }) + pkg.IfIsSetStr(p.OpcachePreload, func(s string) { env[CC_OPCACHE_PRELOAD] = s }) + pkg.IfIsSetStr(p.AsyncAppBucket, func(s string) { env[CC_PHP_ASYNC_APP_BUCKET] = s }) + pkg.IfIsSetStr(p.DevDependencies, func(s string) { env[CC_PHP_DEV_DEPENDENCIES] = s }) + pkg.IfIsSetStr(p.DisableAppBucket, func(s string) { env[CC_PHP_DISABLE_APP_BUCKET] = s }) + pkg.IfIsSetStr(p.PHPVersion, func(s string) { env[CC_PHP_VERSION] = s }) + pkg.IfIsSetI(p.RealpathCacheTTL, func(i int64) { env[CC_REALPATH_CACHE_TTL] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(p.WebRoot, func(s string) { env[CC_WEBROOT] = s }) + pkg.IfIsSetB(p.EnableElasticApmAgent, func(b bool) { env[ENABLE_ELASTIC_APM_AGENT] = strconv.FormatBool(b) }) + pkg.IfIsSetB(p.EnableGrpc, func(b bool) { env[ENABLE_GRPC] = strconv.FormatBool(b) }) + pkg.IfIsSetB(p.EnablePdflib, func(b bool) { env[ENABLE_PDFLIB] = strconv.FormatBool(b) }) + pkg.IfIsSetB(p.EnableRedis, func(b bool) { env[ENABLE_REDIS] = strconv.FormatBool(b) }) + pkg.IfIsSetI(p.HttpTimeout, func(i int64) { env[HTTP_TIMEOUT] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(p.LdaptlsCacert, func(s string) { env[LDAPTLS_CACERT] = s }) + pkg.IfIsSetI(p.MaxInputVars, func(i int64) { env[MAX_INPUT_VARS] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(p.MemoryLimit, func(s string) { env[MEMORY_LIMIT] = s }) + pkg.IfIsSetStr(p.SessionType, func(s string) { env[SESSION_TYPE] = s }) + pkg.IfIsSetB(p.SocksifyEverything, func(b bool) { env[SOCKSIFY_EVERYTHING] = strconv.FormatBool(b) }) + pkg.IfIsSetStr(p.SqreenApiAppName, func(s string) { env[SQREEN_API_APP_NAME] = s }) + pkg.IfIsSetStr(p.SqreenApiToken, func(s string) { env[SQREEN_API_TOKEN] = s }) return env } +// fromEnv iter on environment set on the clever application and +// handle language specific env vars +// put the others on Environment field +func (p *PHP) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + // Parse PHP-specific environment variables + p.AlwaysPopulateRawPostData = pkg.FromStr(m.Pop(ALWAYS_POPULATE_RAW_POST_DATA)) + p.ComposerVersion = pkg.FromStr(m.Pop(CC_COMPOSER_VERSION)) + p.CgiImplementation = pkg.FromStr(m.Pop(CC_CGI_IMPLEMENTATION)) + p.HttpBasicAuth = pkg.FromStr(m.Pop(CC_HTTP_BASIC_AUTH)) + + if size, err := strconv.ParseInt(m.Pop(CC_APACHE_HEADERS_SIZE), 10, 64); err == nil { + p.ApacheHeadersSize = pkg.FromI(size) + } + + p.LdapCaCert = pkg.FromStr(m.Pop(CC_LDAP_CA_CERT)) + p.MtaAuthPassword = pkg.FromStr(m.Pop(CC_MTA_AUTH_PASSWORD)) + p.MtaAuthUser = pkg.FromStr(m.Pop(CC_MTA_AUTH_USER)) + p.MtaServerAuthMethod = pkg.FromStr(m.Pop(CC_MTA_SERVER_AUTH_METHOD)) + p.MtaServerHost = pkg.FromStr(m.Pop(CC_MTA_SERVER_HOST)) + + if port, err := strconv.ParseInt(m.Pop(CC_MTA_SERVER_PORT), 10, 64); err == nil { + p.MtaServerPort = pkg.FromI(port) + } + + p.MtaServerUseTLS = pkg.FromBool(m.Pop(CC_MTA_SERVER_USE_TLS) == "true") + + if buffer, err := strconv.ParseInt(m.Pop(CC_OPCACHE_INTERNED_STRINGS_BUFFER), 10, 64); err == nil { + p.OpcacheInternedStringsBuffer = pkg.FromI(buffer) + } + + if files, err := strconv.ParseInt(m.Pop(CC_OPCACHE_MAX_ACCELERATED_FILES), 10, 64); err == nil { + p.OpcacheMaxAcceleratedFiles = pkg.FromI(files) + } + + p.OpcacheMemory = pkg.FromStr(m.Pop(CC_OPCACHE_MEMORY)) + p.OpcachePreload = pkg.FromStr(m.Pop(CC_OPCACHE_PRELOAD)) + p.AsyncAppBucket = pkg.FromStr(m.Pop(CC_PHP_ASYNC_APP_BUCKET)) + p.DevDependencies = pkg.FromStr(m.Pop(CC_PHP_DEV_DEPENDENCIES)) + p.DisableAppBucket = pkg.FromStr(m.Pop(CC_PHP_DISABLE_APP_BUCKET)) + p.PHPVersion = pkg.FromStr(m.Pop(CC_PHP_VERSION)) + + if ttl, err := strconv.ParseInt(m.Pop(CC_REALPATH_CACHE_TTL), 10, 64); err == nil { + p.RealpathCacheTTL = pkg.FromI(ttl) + } + + p.WebRoot = pkg.FromStr(m.Pop(CC_WEBROOT)) + p.EnableElasticApmAgent = pkg.FromBool(m.Pop(ENABLE_ELASTIC_APM_AGENT) == "true") + p.EnableGrpc = pkg.FromBool(m.Pop(ENABLE_GRPC) == "true") + p.EnablePdflib = pkg.FromBool(m.Pop(ENABLE_PDFLIB) == "true") + p.EnableRedis = pkg.FromBool(m.Pop(ENABLE_REDIS) == "true") + + if timeout, err := strconv.ParseInt(m.Pop(HTTP_TIMEOUT), 10, 64); err == nil { + p.HttpTimeout = pkg.FromI(timeout) + } + + p.LdaptlsCacert = pkg.FromStr(m.Pop(LDAPTLS_CACERT)) + + if vars, err := strconv.ParseInt(m.Pop(MAX_INPUT_VARS), 10, 64); err == nil { + p.MaxInputVars = pkg.FromI(vars) + } + + p.MemoryLimit = pkg.FromStr(m.Pop(MEMORY_LIMIT)) + p.SessionType = pkg.FromStr(m.Pop(SESSION_TYPE)) + p.SocksifyEverything = pkg.FromBool(m.Pop(SOCKSIFY_EVERYTHING) == "true") + p.SqreenApiAppName = pkg.FromStr(m.Pop(SQREEN_API_APP_NAME)) + p.SqreenApiToken = pkg.FromStr(m.Pop(SQREEN_API_TOKEN)) + + // Handle common runtime variables (APP_FOLDER, Hooks, remaining Environment) + p.FromEnvironment(ctx, m) +} + func (p *PHP) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if p.Deployment == nil || p.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/play2/crud.go b/pkg/resources/play2/crud.go index 7af46a8c..73b0ce2d 100644 --- a/pkg/resources/play2/crud.go +++ b/pkg/resources/play2/crud.go @@ -109,15 +109,7 @@ func (r *ResourcePlay2) Read(ctx context.Context, req resource.ReadRequest, resp state.BuildFlavor = readRes.GetBuildFlavor() state.VHosts = helper.VHostsFromAPIHosts(ctx, readRes.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) - - for envName, envValue := range readRes.EnvAsMap() { - switch envName { - case "APP_FOLDER": - state.AppFolder = pkg.FromStr(envValue) - default: - //state.Environment. - } - } + state.fromEnv(ctx, readRes.EnvAsMap()) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } diff --git a/pkg/resources/play2/play2.go b/pkg/resources/play2/play2.go index bb9a0a37..4748fcac 100644 --- a/pkg/resources/play2/play2.go +++ b/pkg/resources/play2/play2.go @@ -10,6 +10,13 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +const ( + CC_SBT_TARGET_BIN = "CC_SBT_TARGET_BIN" + CC_SBT_TARGET_DIR = "CC_SBT_TARGET_DIR" + PLAY1_VERSION = "PLAY1_VERSION" + SBT_DEPLOY_GOAL = "SBT_DEPLOY_GOAL" +) + type ResourcePlay2 struct { helper.Configurer } diff --git a/pkg/resources/play2/schema.go b/pkg/resources/play2/schema.go index acf6d607..f5ce679b 100644 --- a/pkg/resources/play2/schema.go +++ b/pkg/resources/play2/schema.go @@ -3,19 +3,24 @@ package play2 import ( "context" _ "embed" - "maps" "github.com/go-git/go-git/v5/plumbing/transport/http" "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/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Play2 struct { attributes.Runtime + Play1Version types.String `tfsdk:"play1_version"` + SbtDeployGoal types.String `tfsdk:"sbt_deploy_goal"` + SbtTargetBin types.String `tfsdk:"sbt_target_bin"` + SbtTargetDir types.String `tfsdk:"sbt_target_dir"` } type Play2V0 struct { @@ -32,8 +37,29 @@ func (r ResourcePlay2) Schema(ctx context.Context, req resource.SchemaRequest, r var schemaPlay2 = schema.Schema{ Version: 1, MarkdownDescription: play2Doc, - Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{}), - Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), + Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + // PLAY1_VERSION + "play1_version": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which play1 version to use between `1.2`, `1.3`, `1.4` and `1.5`", + }, + // SBT_DEPLOY_GOAL + "sbt_deploy_goal": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which SBT goals to run during build (default: `stage`)", + }, + // CC_SBT_TARGET_BIN + "sbt_target_bin": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define the bin to pick in the `CC_SBT_TARGET_DIR`", + }, + // CC_SBT_TARGET_DIR + "sbt_target_dir": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define the folder the `target` dir is in (default: `.`)", + }, + }), + Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), } var schemaPlay2V0 = schema.Schema{ @@ -44,21 +70,30 @@ var schemaPlay2V0 = schema.Schema{ } func (plan *Play2) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(plan.Environment.ElementsAs(ctx, &customEnv, false)...) + env := plan.ToEnv(ctx, diags) if diags.HasError() { return env } - maps.Copy(env, customEnv) - pkg.IfIsSetStr(plan.AppFolder, func(s string) { env["APP_FOLDER"] = s }) + pkg.IfIsSetStr(plan.Play1Version, func(s string) { env[PLAY1_VERSION] = s }) + pkg.IfIsSetStr(plan.SbtDeployGoal, func(s string) { env[SBT_DEPLOY_GOAL] = s }) + pkg.IfIsSetStr(plan.SbtTargetBin, func(s string) { env[CC_SBT_TARGET_BIN] = s }) + pkg.IfIsSetStr(plan.SbtTargetDir, func(s string) { env[CC_SBT_TARGET_DIR] = s }) + return env } +func (play2 *Play2) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + play2.Play1Version = pkg.FromStr(m.Pop(PLAY1_VERSION)) + play2.SbtDeployGoal = pkg.FromStr(m.Pop(SBT_DEPLOY_GOAL)) + play2.SbtTargetBin = pkg.FromStr(m.Pop(CC_SBT_TARGET_BIN)) + play2.SbtTargetDir = pkg.FromStr(m.Pop(CC_SBT_TARGET_DIR)) + + play2.FromEnvironment(ctx, m) +} + func (play2 *Play2) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if play2.Deployment == nil || play2.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/postgresql/crud.go b/pkg/resources/postgresql/crud.go index bbe2eb41..6ba64730 100644 --- a/pkg/resources/postgresql/crud.go +++ b/pkg/resources/postgresql/crud.go @@ -51,7 +51,7 @@ func (r *ResourcePostgreSQL) Create(ctx context.Context, req resource.CreateRequ addonsProviders := addonsProvidersRes.Payload() prov := pkg.LookupAddonProvider(*addonsProviders, "postgresql-addon") - plan := pkg.LookupProviderPlan(prov, pg.Plan.ValueString()) + plan := pkg.LookupProviderPlan(prov, strings.ToLower(pg.Plan.ValueString())) if plan == nil { resp.Diagnostics.AddError("failed to find plan", "expect: "+strings.Join(pkg.ProviderPlansAsList(prov), ", ")+", got: "+pg.Plan.String()) return @@ -85,7 +85,8 @@ func (r *ResourcePostgreSQL) Create(ctx context.Context, req resource.CreateRequ pg.ID = pkg.FromStr(createdPg.RealID) pg.CreationDate = pkg.FromI(createdPg.CreationDate) - pg.Plan = pkg.FromStr(createdPg.Plan.Slug) + // Normalize plan to lowercase + pg.Plan = pkg.FromStr(strings.ToLower(pg.Plan.ValueString())) resp.Diagnostics.Append(resp.State.Set(ctx, pg)...) @@ -99,6 +100,7 @@ func (r *ResourcePostgreSQL) Create(ctx context.Context, req resource.CreateRequ tflog.Debug(ctx, "API response", map[string]any{ "payload": fmt.Sprintf("%+v", addonPG), }) + pg.Plan = pkg.FromStr(strings.ToLower(addonPG.Plan)) pg.Host = pkg.FromStr(addonPG.Host) pg.Port = pkg.FromI(int64(addonPG.Port)) pg.Database = pkg.FromStr(addonPG.Database) @@ -166,7 +168,7 @@ func (r *ResourcePostgreSQL) Read(ctx context.Context, req resource.ReadRequest, tflog.Debug(ctx, "API", map[string]any{"pg": addonPG}) pg.ID = pkg.FromStr(realID) pg.Name = pkg.FromStr(addon.Name) - pg.Plan = pkg.FromStr(addonPG.Plan) + pg.Plan = pkg.FromStr(strings.ToLower(addonPG.Plan)) pg.Region = pkg.FromStr(addonPG.Zone) pg.CreationDate = pkg.FromI(addon.CreationDate) pg.Host = pkg.FromStr(addonPG.Host) diff --git a/pkg/resources/postgresql/postgresql_test.go b/pkg/resources/postgresql/postgresql_test.go index e9f9642c..3561a70b 100644 --- a/pkg/resources/postgresql/postgresql_test.go +++ b/pkg/resources/postgresql/postgresql_test.go @@ -35,7 +35,7 @@ func TestAccPostgreSQL_basic(t *testing.T) { helper.SetKeyValues(map[string]any{ "name": rName, "region": "par", - "plan": "dev", + "plan": "xs_tny", // Plan names are case-insensitive in the API "backup": true, })) @@ -77,7 +77,7 @@ func TestAccPostgreSQL_basic(t *testing.T) { statecheck.ExpectKnownValue(fullName, tfjsonpath.New("database"), knownvalue.StringRegexp(regexp.MustCompile(`^[a-zA-Z0-9]+$`))), statecheck.ExpectKnownValue(fullName, tfjsonpath.New("user"), knownvalue.StringRegexp(regexp.MustCompile(`^[a-zA-Z0-9]+$`))), statecheck.ExpectSensitiveValue(fullName, tfjsonpath.New("password")), - statecheck.ExpectKnownValue(fullName, tfjsonpath.New("plan"), knownvalue.StringExact("dev")), + statecheck.ExpectKnownValue(fullName, tfjsonpath.New("plan"), knownvalue.StringExact("xs_tny")), // Normalized to lowercase statecheck.ExpectKnownValue(fullName, tfjsonpath.New("backup"), knownvalue.Bool(true)), }, }, { @@ -146,7 +146,7 @@ func TestAccPostgreSQL_RefreshDeleted(t *testing.T) { PreCheck: tests.ExpectOrganisation(t), CheckDestroy: func(state *terraform.State) error { for _, resource := range state.RootModule().Resources { - addonId, err := tmp.RealIDToAddonID(context.Background(), cc, tests.ORGANISATION, resource.Primary.ID) + addonID, err := tmp.RealIDToAddonID(context.Background(), cc, tests.ORGANISATION, resource.Primary.ID) if err != nil { if strings.Contains(err.Error(), "not found") { continue @@ -154,7 +154,7 @@ func TestAccPostgreSQL_RefreshDeleted(t *testing.T) { return fmt.Errorf("failed to get addon ID: %s", err.Error()) } - res := tmp.GetPostgreSQL(context.Background(), cc, addonId) + res := tmp.GetPostgreSQL(context.Background(), cc, addonID) if res.IsNotFoundError() { continue } diff --git a/pkg/resources/python/crud.go b/pkg/resources/python/crud.go index 0a595eec..ead6e51f 100644 --- a/pkg/resources/python/crud.go +++ b/pkg/resources/python/crud.go @@ -112,6 +112,8 @@ func (r *ResourcePython) Read(ctx context.Context, req resource.ReadRequest, res state.StickySessions = pkg.FromBool(appRes.App.StickySessions) state.RedirectHTTPS = pkg.FromBool(application.ToForceHTTPS(appRes.App.ForceHTTPS)) + state.fromEnv(ctx, appRes.EnvAsMap()) + state.VHosts = helper.VHostsFromAPIHosts(ctx, appRes.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) diags = resp.State.Set(ctx, state) diff --git a/pkg/resources/python/python.go b/pkg/resources/python/python.go index e3c0a6c9..46c662d1 100644 --- a/pkg/resources/python/python.go +++ b/pkg/resources/python/python.go @@ -13,6 +13,38 @@ type ResourcePython struct { helper.Configurer } +const ( + CC_HTTP_BASIC_AUTH = "CC_HTTP_BASIC_AUTH" + CC_NGINX_PROXY_BUFFERS = "CC_NGINX_PROXY_BUFFERS" + CC_NGINX_PROXY_BUFFER_SIZE = "CC_NGINX_PROXY_BUFFER_SIZE" + CC_PIP_REQUIREMENTS_FILE = "CC_PIP_REQUIREMENTS_FILE" + CC_PYTHON_BACKEND = "CC_PYTHON_BACKEND" + CC_PYTHON_CELERY_LOGFILE = "CC_PYTHON_CELERY_LOGFILE" + CC_PYTHON_CELERY_MODULE = "CC_PYTHON_CELERY_MODULE" + CC_PYTHON_CELERY_USE_BEAT = "CC_PYTHON_CELERY_USE_BEAT" + CC_PYTHON_MANAGE_TASKS = "CC_PYTHON_MANAGE_TASKS" + CC_PYTHON_MODULE = "CC_PYTHON_MODULE" + CC_PYTHON_USE_GEVENT = "CC_PYTHON_USE_GEVENT" + CC_PYTHON_VERSION = "CC_PYTHON_VERSION" + CC_GUNICORN_TIMEOUT = "CC_GUNICORN_TIMEOUT" + ENABLE_GZIP_COMPRESSION = "ENABLE_GZIP_COMPRESSION" + GZIP_TYPES = "GZIP_TYPES" + GUNICORN_WORKER_CLASS = "GUNICORN_WORKER_CLASS" + HARAKIRI = "HARAKIRI" + NGINX_READ_TIMEOUT = "NGINX_READ_TIMEOUT" + PYTHON_SETUP_PY_GOAL = "PYTHON_SETUP_PY_GOAL" + STATIC_FILES_PATH = "STATIC_FILES_PATH" + STATIC_URL_PREFIX = "STATIC_URL_PREFIX" + STATIC_WEBROOT = "STATIC_WEBROOT" + UWSGI_ASYNC = "UWSGI_ASYNC" + UWSGI_ASYNC_ENGINE = "UWSGI_ASYNC_ENGINE" + UWSGI_INTERCEPT_ERRORS = "UWSGI_INTERCEPT_ERRORS" + WSGI_BUFFER_SIZE = "WSGI_BUFFER_SIZE" + WSGI_POST_BUFFERING = "WSGI_POST_BUFFERING" + WSGI_THREADS = "WSGI_THREADS" + WSGI_WORKERS = "WSGI_WORKERS" +) + func NewResourcePython() resource.Resource { return &ResourcePython{} } diff --git a/pkg/resources/python/schema.go b/pkg/resources/python/schema.go index 8848139b..dd7a149b 100644 --- a/pkg/resources/python/schema.go +++ b/pkg/resources/python/schema.go @@ -3,21 +3,52 @@ package python import ( "context" _ "embed" + "fmt" + "strconv" "github.com/go-git/go-git/v5/plumbing/transport/http" "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/booldefault" "github.com/hashicorp/terraform-plugin-framework/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Python struct { attributes.Runtime - PythonVersion types.String `tfsdk:"python_version"` - PipRequirements types.String `tfsdk:"pip_requirements"` + HttpBasicAuth types.String `tfsdk:"http_basic_auth"` + NginxProxyBuffers types.String `tfsdk:"nginx_proxy_buffers"` + NginxProxyBufferSize types.String `tfsdk:"nginx_proxy_buffer_size"` + PipRequirements types.String `tfsdk:"pip_requirements"` + PythonBackend types.String `tfsdk:"python_backend"` + CeleryLogfile types.String `tfsdk:"celery_logfile"` + CeleryModule types.String `tfsdk:"celery_module"` + CeleryUseBeat types.Bool `tfsdk:"celery_use_beat"` + ManageTasks types.String `tfsdk:"manage_tasks"` + PythonModule types.String `tfsdk:"python_module"` + UseGevent types.Bool `tfsdk:"use_gevent"` + PythonVersion types.String `tfsdk:"python_version"` + GunicornTimeout types.Int64 `tfsdk:"gunicorn_timeout"` + EnableGzipCompression types.Bool `tfsdk:"enable_gzip_compression"` + GzipTypes types.String `tfsdk:"gzip_types"` + GunicornWorkerClass types.String `tfsdk:"gunicorn_worker_class"` + Harakiri types.Int64 `tfsdk:"harakiri"` + NginxReadTimeout types.Int64 `tfsdk:"nginx_read_timeout"` + SetupPyGoal types.String `tfsdk:"setup_py_goal"` + StaticFilesPath types.String `tfsdk:"static_files_path"` + StaticURLPrefix types.String `tfsdk:"static_url_prefix"` + StaticWebroot types.String `tfsdk:"static_webroot"` + UwsgiAsync types.Int64 `tfsdk:"uwsgi_async"` + UwsgiAsyncEngine types.String `tfsdk:"uwsgi_async_engine"` + UwsgiInterceptErrors types.Bool `tfsdk:"uwsgi_intercept_errors"` + WsgiBufferSize types.Int64 `tfsdk:"wsgi_buffer_size"` + WsgiPostBuffering types.Int64 `tfsdk:"wsgi_post_buffering"` + WsgiThreads types.Int64 `tfsdk:"wsgi_threads"` + WsgiWorkers types.Int64 `tfsdk:"wsgi_workers"` } type PythonV0 struct { @@ -37,15 +68,130 @@ var schemaPythonV1 = schema.Schema{ Version: 1, MarkdownDescription: pythonDoc, Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ - // CC_PYTHON_VERSION - "python_version": schema.StringAttribute{ + "http_basic_auth": schema.StringAttribute{ Optional: true, - MarkdownDescription: "Python version >= 2.7", + Sensitive: true, + MarkdownDescription: "Restrict HTTP access to your application. Example: `login:password`. Multiple credentials can be defined using `CC_HTTP_BASIC_AUTH_n`", + }, + "nginx_proxy_buffers": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Configures the number and size of buffers for reading responses from the proxied server", + }, + "nginx_proxy_buffer_size": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Sets the size of the buffer for the initial part of the response from the proxied server", }, - // CC_PIP_REQUIREMENTS_FILE "pip_requirements": schema.StringAttribute{ Optional: true, - MarkdownDescription: "Define a custom requirements.txt file (default: requirements.txt)", + MarkdownDescription: "Specifies a custom requirements.txt file for package installation. Default is `requirements.txt`", + }, + "python_backend": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Selects the Python backend. Options include `daphne`, `gunicorn`, `uvicorn`, and `uwsgi`. Default is `uwsgi`", + }, + "celery_logfile": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Sets the relative path to the Celery log file (e.g., `/path/to/logdir`)", + }, + "celery_module": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Specifies the Celery module to start", + }, + "celery_use_beat": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Set to `true` to enable Celery Beat support", + }, + "manage_tasks": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "A comma-separated list of Django `manage.py` tasks to execute", + }, + "python_module": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Defines the Python module to start with, including the path to the application object. Example: `app.server:app` for a `server.py` file in an `/app` folder", + }, + "use_gevent": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Set to `true` to enable Gevent support", + }, + "python_version": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Selects the Python version. Refer to supported versions documentation", + }, + "gunicorn_timeout": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Timeout for Gunicorn workers. Default is `180`", + }, + "enable_gzip_compression": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Set to `true` to enable Gzip compression via Nginx", + }, + "gzip_types": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Defines the MIME types to be compressed by Gzip. Default is `text/* application/json application/xml application/javascript image/svg+xml`", + }, + "gunicorn_worker_class": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Gunicorn worker class (e.g., `gevent`, `sync`)", + }, + "harakiri": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Timeout in seconds after which an unresponsive process is killed. Default is `180`", + }, + "nginx_read_timeout": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Read timeout in seconds for Nginx. Default is `300`", + }, + "setup_py_goal": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "A custom goal to execute after `requirements.txt` installation", + }, + "static_files_path": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The relative path to the directory containing static files (e.g., `path/to/static`)", + }, + "static_url_prefix": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The URL path prefix for serving static files. Commonly set to `/public`", + }, + "static_webroot": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Specifies the web root for static files", + }, + "uwsgi_async": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Configures the number of cores for uWSGI asynchronous/non-blocking modes", + }, + "uwsgi_async_engine": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Selects the asynchronous engine for uWSGI (optional)", + }, + "uwsgi_intercept_errors": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + MarkdownDescription: "Enables or disables error interception in uWSGI", + }, + "wsgi_buffer_size": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Buffer size in bytes for uploads. Default is `4096`", + }, + "wsgi_post_buffering": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Maximum size in bytes for request headers. Default is `4096`", + }, + "wsgi_threads": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Number of threads per worker. Defaults to automatic setup based on scaler size", + }, + "wsgi_workers": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Number of workers. Defaults to automatic setup based on scaler size", }, }), Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), @@ -70,25 +216,114 @@ var schemaPythonV0 = schema.Schema{ } func (py Python) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(py.Environment.ElementsAs(ctx, &customEnv, false)...) + // Start with common runtime environment variables (APP_FOLDER, Hooks, Environment) + env := py.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) - pkg.IfIsSetStr(py.AppFolder, func(s string) { env["APP_FOLDER"] = s }) - pkg.IfIsSetStr(py.PythonVersion, func(version string) { env["CC_PYTHON_VERSION"] = version }) - pkg.IfIsSetStr(py.PipRequirements, func(pipReqFile string) { env["CC_PIP_REQUIREMENTS_FILE"] = pipReqFile }) + // Add Python-specific environment variables + pkg.IfIsSetStr(py.HttpBasicAuth, func(s string) { env[CC_HTTP_BASIC_AUTH] = s }) + pkg.IfIsSetStr(py.NginxProxyBuffers, func(s string) { env[CC_NGINX_PROXY_BUFFERS] = s }) + pkg.IfIsSetStr(py.NginxProxyBufferSize, func(s string) { env[CC_NGINX_PROXY_BUFFER_SIZE] = s }) + pkg.IfIsSetStr(py.PipRequirements, func(s string) { env[CC_PIP_REQUIREMENTS_FILE] = s }) + pkg.IfIsSetStr(py.PythonBackend, func(s string) { env[CC_PYTHON_BACKEND] = s }) + pkg.IfIsSetStr(py.CeleryLogfile, func(s string) { env[CC_PYTHON_CELERY_LOGFILE] = s }) + pkg.IfIsSetStr(py.CeleryModule, func(s string) { env[CC_PYTHON_CELERY_MODULE] = s }) + pkg.IfIsSetB(py.CeleryUseBeat, func(b bool) { env[CC_PYTHON_CELERY_USE_BEAT] = strconv.FormatBool(b) }) + pkg.IfIsSetStr(py.ManageTasks, func(s string) { env[CC_PYTHON_MANAGE_TASKS] = s }) + pkg.IfIsSetStr(py.PythonModule, func(s string) { env[CC_PYTHON_MODULE] = s }) + pkg.IfIsSetB(py.UseGevent, func(b bool) { env[CC_PYTHON_USE_GEVENT] = strconv.FormatBool(b) }) + pkg.IfIsSetStr(py.PythonVersion, func(s string) { env[CC_PYTHON_VERSION] = s }) + pkg.IfIsSetI(py.GunicornTimeout, func(i int64) { env[CC_GUNICORN_TIMEOUT] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetB(py.EnableGzipCompression, func(b bool) { env[ENABLE_GZIP_COMPRESSION] = strconv.FormatBool(b) }) + pkg.IfIsSetStr(py.GzipTypes, func(s string) { env[GZIP_TYPES] = s }) + pkg.IfIsSetStr(py.GunicornWorkerClass, func(s string) { env[GUNICORN_WORKER_CLASS] = s }) + pkg.IfIsSetI(py.Harakiri, func(i int64) { env[HARAKIRI] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetI(py.NginxReadTimeout, func(i int64) { env[NGINX_READ_TIMEOUT] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(py.SetupPyGoal, func(s string) { env[PYTHON_SETUP_PY_GOAL] = s }) + pkg.IfIsSetStr(py.StaticFilesPath, func(s string) { env[STATIC_FILES_PATH] = s }) + pkg.IfIsSetStr(py.StaticURLPrefix, func(s string) { env[STATIC_URL_PREFIX] = s }) + pkg.IfIsSetStr(py.StaticWebroot, func(s string) { env[STATIC_WEBROOT] = s }) + pkg.IfIsSetI(py.UwsgiAsync, func(i int64) { env[UWSGI_ASYNC] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(py.UwsgiAsyncEngine, func(s string) { env[UWSGI_ASYNC_ENGINE] = s }) + pkg.IfIsSetB(py.UwsgiInterceptErrors, func(b bool) { env[UWSGI_INTERCEPT_ERRORS] = strconv.FormatBool(b) }) + pkg.IfIsSetI(py.WsgiBufferSize, func(i int64) { env[WSGI_BUFFER_SIZE] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetI(py.WsgiPostBuffering, func(i int64) { env[WSGI_POST_BUFFERING] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetI(py.WsgiThreads, func(i int64) { env[WSGI_THREADS] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetI(py.WsgiWorkers, func(i int64) { env[WSGI_WORKERS] = fmt.Sprintf("%d", i) }) - env = pkg.Merge(env, py.Hooks.ToEnv()) return env } +// fromEnv iter on environment set on the clever application and +// handle language specific env vars +// put the others on Environment field +func (py *Python) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + // Parse Python-specific environment variables + py.HttpBasicAuth = pkg.FromStr(m.Pop(CC_HTTP_BASIC_AUTH)) + py.NginxProxyBuffers = pkg.FromStr(m.Pop(CC_NGINX_PROXY_BUFFERS)) + py.NginxProxyBufferSize = pkg.FromStr(m.Pop(CC_NGINX_PROXY_BUFFER_SIZE)) + py.PipRequirements = pkg.FromStr(m.Pop(CC_PIP_REQUIREMENTS_FILE)) + py.PythonBackend = pkg.FromStr(m.Pop(CC_PYTHON_BACKEND)) + py.CeleryLogfile = pkg.FromStr(m.Pop(CC_PYTHON_CELERY_LOGFILE)) + py.CeleryModule = pkg.FromStr(m.Pop(CC_PYTHON_CELERY_MODULE)) + py.CeleryUseBeat = pkg.FromBool(m.Pop(CC_PYTHON_CELERY_USE_BEAT) == "true") + py.ManageTasks = pkg.FromStr(m.Pop(CC_PYTHON_MANAGE_TASKS)) + py.PythonModule = pkg.FromStr(m.Pop(CC_PYTHON_MODULE)) + py.UseGevent = pkg.FromBool(m.Pop(CC_PYTHON_USE_GEVENT) == "true") + py.PythonVersion = pkg.FromStr(m.Pop(CC_PYTHON_VERSION)) + + if timeout, err := strconv.ParseInt(m.Pop(CC_GUNICORN_TIMEOUT), 10, 64); err == nil { + py.GunicornTimeout = pkg.FromI(timeout) + } + + py.EnableGzipCompression = pkg.FromBool(m.Pop(ENABLE_GZIP_COMPRESSION) == "true") + py.GzipTypes = pkg.FromStr(m.Pop(GZIP_TYPES)) + py.GunicornWorkerClass = pkg.FromStr(m.Pop(GUNICORN_WORKER_CLASS)) + + if harakiri, err := strconv.ParseInt(m.Pop(HARAKIRI), 10, 64); err == nil { + py.Harakiri = pkg.FromI(harakiri) + } + + if nginxTimeout, err := strconv.ParseInt(m.Pop(NGINX_READ_TIMEOUT), 10, 64); err == nil { + py.NginxReadTimeout = pkg.FromI(nginxTimeout) + } + + py.SetupPyGoal = pkg.FromStr(m.Pop(PYTHON_SETUP_PY_GOAL)) + py.StaticFilesPath = pkg.FromStr(m.Pop(STATIC_FILES_PATH)) + py.StaticURLPrefix = pkg.FromStr(m.Pop(STATIC_URL_PREFIX)) + py.StaticWebroot = pkg.FromStr(m.Pop(STATIC_WEBROOT)) + + if async, err := strconv.ParseInt(m.Pop(UWSGI_ASYNC), 10, 64); err == nil { + py.UwsgiAsync = pkg.FromI(async) + } + + py.UwsgiAsyncEngine = pkg.FromStr(m.Pop(UWSGI_ASYNC_ENGINE)) + py.UwsgiInterceptErrors = pkg.FromBool(m.Pop(UWSGI_INTERCEPT_ERRORS) == "true") + + if bufferSize, err := strconv.ParseInt(m.Pop(WSGI_BUFFER_SIZE), 10, 64); err == nil { + py.WsgiBufferSize = pkg.FromI(bufferSize) + } + + if postBuffering, err := strconv.ParseInt(m.Pop(WSGI_POST_BUFFERING), 10, 64); err == nil { + py.WsgiPostBuffering = pkg.FromI(postBuffering) + } + + if threads, err := strconv.ParseInt(m.Pop(WSGI_THREADS), 10, 64); err == nil { + py.WsgiThreads = pkg.FromI(threads) + } + + if workers, err := strconv.ParseInt(m.Pop(WSGI_WORKERS), 10, 64); err == nil { + py.WsgiWorkers = pkg.FromI(workers) + } + + // Handle common runtime variables (APP_FOLDER, Hooks, remaining Environment) + py.FromEnvironment(ctx, m) +} + func (py Python) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if py.Deployment == nil || py.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/redis/crud.go b/pkg/resources/redis/crud.go index 5470a23e..844285c3 100644 --- a/pkg/resources/redis/crud.go +++ b/pkg/resources/redis/crud.go @@ -132,7 +132,7 @@ func (r *ResourceRedis) Read(ctx context.Context, req resource.ReadRequest, resp rd.Name = pkg.FromStr(addonRD.Name) rd.Host = envAsMap["REDIS_HOST"] - rd.Plan = pkg.FromStr(addonRD.Plan.Slug) + rd.Plan = pkg.FromStr(strings.ToLower(addonRD.Plan.Slug)) rd.Port = pkg.FromI(port) rd.Region = pkg.FromStr(addonRD.Region) rd.Token = envAsMap["REDIS_PASSWORD"] diff --git a/pkg/resources/ruby/crud.go b/pkg/resources/ruby/crud.go index 3f3a8778..9a9ec202 100644 --- a/pkg/resources/ruby/crud.go +++ b/pkg/resources/ruby/crud.go @@ -98,8 +98,8 @@ func (r *ResourceRuby) Read(ctx context.Context, req resource.ReadRequest, resp } state.DeployURL = pkg.FromStr(appRes.App.DeployURL) - state.VHosts = helper.VHostsFromAPIHosts(ctx, appRes.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) + state.fromEnv(ctx, appRes.EnvAsMap()) diags = resp.State.Set(ctx, state) resp.Diagnostics.Append(diags...) diff --git a/pkg/resources/ruby/ruby.go b/pkg/resources/ruby/ruby.go index 4a98023b..29793243 100644 --- a/pkg/resources/ruby/ruby.go +++ b/pkg/resources/ruby/ruby.go @@ -10,6 +10,25 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +const ( + CC_ENABLE_SIDEKIQ = "CC_ENABLE_SIDEKIQ" + CC_HTTP_BASIC_AUTH = "CC_HTTP_BASIC_AUTH" + CC_NGINX_PROXY_BUFFERS = "CC_NGINX_PROXY_BUFFERS" + CC_NGINX_PROXY_BUFFER_SIZE = "CC_NGINX_PROXY_BUFFER_SIZE" + CC_RACKUP_SERVER = "CC_RACKUP_SERVER" + CC_RAKEGOALS = "CC_RAKEGOALS" + CC_RUBY_VERSION = "CC_RUBY_VERSION" + CC_SIDEKIQ_FILES = "CC_SIDEKIQ_FILES" + ENABLE_GZIP_COMPRESSION = "ENABLE_GZIP_COMPRESSION" + GZIP_TYPES = "GZIP_TYPES" + NGINX_READ_TIMEOUT = "NGINX_READ_TIMEOUT" + RACK_ENV = "RACK_ENV" + RAILS_ENV = "RAILS_ENV" + STATIC_FILES_PATH = "STATIC_FILES_PATH" + STATIC_URL_PREFIX = "STATIC_URL_PREFIX" + STATIC_WEBROOT = "STATIC_WEBROOT" +) + type ResourceRuby struct { helper.Configurer } diff --git a/pkg/resources/ruby/schema.go b/pkg/resources/ruby/schema.go index f83999c6..6c0efbed 100644 --- a/pkg/resources/ruby/schema.go +++ b/pkg/resources/ruby/schema.go @@ -9,10 +9,12 @@ import ( "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/booldefault" "github.com/hashicorp/terraform-plugin-framework/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Ruby struct { @@ -74,6 +76,8 @@ var schemaRuby = schema.Schema{ // CC_ENABLE_SIDEKIQ "enable_sidekiq": schema.BoolAttribute{ Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), MarkdownDescription: "Enable Sidekiq background process", }, // CC_RACKUP_SERVER @@ -110,6 +114,8 @@ var schemaRuby = schema.Schema{ // ENABLE_GZIP_COMPRESSION "enable_gzip_compression": schema.BoolAttribute{ Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), MarkdownDescription: "Set to true to gzip-compress through Nginx", }, // GZIP_TYPES @@ -163,6 +169,8 @@ var schemaRubyV0 = schema.Schema{ // CC_ENABLE_SIDEKIQ "enable_sidekiq": schema.BoolAttribute{ Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), MarkdownDescription: "Enable Sidekiq background process", }, // CC_RACKUP_SERVER @@ -199,6 +207,8 @@ var schemaRubyV0 = schema.Schema{ // ENABLE_GZIP_COMPRESSION "enable_gzip_compression": schema.BoolAttribute{ Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), MarkdownDescription: "Set to true to gzip-compress through Nginx", }, // GZIP_TYPES @@ -241,47 +251,60 @@ var schemaRubyV0 = schema.Schema{ } func (ruby Ruby) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(ruby.Environment.ElementsAs(ctx, &customEnv, false)...) + env := ruby.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) - pkg.IfIsSetStr(ruby.AppFolder, func(s string) { env["APP_FOLDER"] = s }) - pkg.IfIsSetStr(ruby.RubyVersion, func(s string) { env["CC_RUBY_VERSION"] = s }) - pkg.IfIsSetB(ruby.EnableSidekiq, func(b bool) { - if b { - env["CC_ENABLE_SIDEKIQ"] = "true" - } - }) - pkg.IfIsSetStr(ruby.RackupServer, func(s string) { env["CC_RACKUP_SERVER"] = s }) - pkg.IfIsSetStr(ruby.RakeGoals, func(s string) { env["CC_RAKEGOALS"] = s }) - pkg.IfIsSetStr(ruby.SidekiqFiles, func(s string) { env["CC_SIDEKIQ_FILES"] = s }) - pkg.IfIsSetStr(ruby.HTTPBasicAuth, func(s string) { env["CC_HTTP_BASIC_AUTH"] = s }) - pkg.IfIsSetStr(ruby.NginxProxyBuffers, func(s string) { env["CC_NGINX_PROXY_BUFFERS"] = s }) - pkg.IfIsSetStr(ruby.NginxProxyBufferSize, func(s string) { env["CC_NGINX_PROXY_BUFFER_SIZE"] = s }) - pkg.IfIsSetB(ruby.EnableGzipCompression, func(b bool) { - if b { - env["ENABLE_GZIP_COMPRESSION"] = "true" - } - }) - pkg.IfIsSetStr(ruby.GzipTypes, func(s string) { env["GZIP_TYPES"] = s }) - pkg.IfIsSetI(ruby.NginxReadTimeout, func(i int64) { env["NGINX_READ_TIMEOUT"] = strconv.FormatInt(i, 10) }) - pkg.IfIsSetStr(ruby.RackEnv, func(s string) { env["RACK_ENV"] = s }) - pkg.IfIsSetStr(ruby.RailsEnv, func(s string) { env["RAILS_ENV"] = s }) - pkg.IfIsSetStr(ruby.StaticFilesPath, func(s string) { env["STATIC_FILES_PATH"] = s }) - pkg.IfIsSetStr(ruby.StaticURLPrefix, func(s string) { env["STATIC_URL_PREFIX"] = s }) - pkg.IfIsSetStr(ruby.StaticWebroot, func(s string) { env["STATIC_WEBROOT"] = s }) - env = pkg.Merge(env, ruby.Hooks.ToEnv()) + pkg.IfIsSetStr(ruby.RubyVersion, func(s string) { env[CC_RUBY_VERSION] = s }) + pkg.IfIsSetB(ruby.EnableSidekiq, func(b bool) { env[CC_ENABLE_SIDEKIQ] = strconv.FormatBool(b) }) + pkg.IfIsSetStr(ruby.RackupServer, func(s string) { env[CC_RACKUP_SERVER] = s }) + pkg.IfIsSetStr(ruby.RakeGoals, func(s string) { env[CC_RAKEGOALS] = s }) + pkg.IfIsSetStr(ruby.SidekiqFiles, func(s string) { env[CC_SIDEKIQ_FILES] = s }) + pkg.IfIsSetStr(ruby.HTTPBasicAuth, func(s string) { env[CC_HTTP_BASIC_AUTH] = s }) + pkg.IfIsSetStr(ruby.NginxProxyBuffers, func(s string) { env[CC_NGINX_PROXY_BUFFERS] = s }) + pkg.IfIsSetStr(ruby.NginxProxyBufferSize, func(s string) { env[CC_NGINX_PROXY_BUFFER_SIZE] = s }) + pkg.IfIsSetB(ruby.EnableGzipCompression, func(b bool) { env[ENABLE_GZIP_COMPRESSION] = strconv.FormatBool(b) }) + pkg.IfIsSetStr(ruby.GzipTypes, func(s string) { env[GZIP_TYPES] = s }) + pkg.IfIsSetI(ruby.NginxReadTimeout, func(i int64) { env[NGINX_READ_TIMEOUT] = strconv.FormatInt(i, 10) }) + pkg.IfIsSetStr(ruby.RackEnv, func(s string) { env[RACK_ENV] = s }) + pkg.IfIsSetStr(ruby.RailsEnv, func(s string) { env[RAILS_ENV] = s }) + pkg.IfIsSetStr(ruby.StaticFilesPath, func(s string) { env[STATIC_FILES_PATH] = s }) + pkg.IfIsSetStr(ruby.StaticURLPrefix, func(s string) { env[STATIC_URL_PREFIX] = s }) + pkg.IfIsSetStr(ruby.StaticWebroot, func(s string) { env[STATIC_WEBROOT] = s }) return env } +func (ruby *Ruby) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + ruby.RubyVersion = pkg.FromStr(m.Pop(CC_RUBY_VERSION)) + if sidekiq, err := strconv.ParseBool(m.Pop(CC_ENABLE_SIDEKIQ)); err == nil { + ruby.EnableSidekiq = pkg.FromBool(sidekiq) + } + ruby.RackupServer = pkg.FromStr(m.Pop(CC_RACKUP_SERVER)) + ruby.RakeGoals = pkg.FromStr(m.Pop(CC_RAKEGOALS)) + ruby.SidekiqFiles = pkg.FromStr(m.Pop(CC_SIDEKIQ_FILES)) + ruby.HTTPBasicAuth = pkg.FromStr(m.Pop(CC_HTTP_BASIC_AUTH)) + ruby.NginxProxyBuffers = pkg.FromStr(m.Pop(CC_NGINX_PROXY_BUFFERS)) + ruby.NginxProxyBufferSize = pkg.FromStr(m.Pop(CC_NGINX_PROXY_BUFFER_SIZE)) + if gzip, err := strconv.ParseBool(m.Pop(ENABLE_GZIP_COMPRESSION)); err == nil { + ruby.EnableGzipCompression = pkg.FromBool(gzip) + } + ruby.GzipTypes = pkg.FromStr(m.Pop(GZIP_TYPES)) + if timeout, err := strconv.ParseInt(m.Pop(NGINX_READ_TIMEOUT), 10, 64); err == nil { + ruby.NginxReadTimeout = pkg.FromI(timeout) + } + ruby.RackEnv = pkg.FromStr(m.Pop(RACK_ENV)) + ruby.RailsEnv = pkg.FromStr(m.Pop(RAILS_ENV)) + ruby.StaticFilesPath = pkg.FromStr(m.Pop(STATIC_FILES_PATH)) + ruby.StaticURLPrefix = pkg.FromStr(m.Pop(STATIC_URL_PREFIX)) + ruby.StaticWebroot = pkg.FromStr(m.Pop(STATIC_WEBROOT)) + + ruby.FromEnvironment(ctx, m) +} + func (ruby Ruby) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if ruby.Deployment == nil || ruby.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/rust/crud.go b/pkg/resources/rust/crud.go index 7ee6d511..9c5acf53 100644 --- a/pkg/resources/rust/crud.go +++ b/pkg/resources/rust/crud.go @@ -3,7 +3,6 @@ package rust import ( "context" "reflect" - "strings" "github.com/hashicorp/terraform-plugin-framework/resource" "go.clever-cloud.com/terraform-provider/pkg" @@ -109,10 +108,7 @@ func (r *ResourceRust) Read(ctx context.Context, req resource.ReadRequest, res * state.RedirectHTTPS = pkg.FromBool(application.ToForceHTTPS(appRes.App.ForceHTTPS)) state.VHosts = helper.VHostsFromAPIHosts(ctx, appRes.App.Vhosts.AsString(), state.VHosts, &res.Diagnostics) - - if env := appRes.EnvAsMap(); env[CC_RUST_FEATURES] != "" { - state.Features = pkg.FromSetString(strings.Split(env[CC_RUST_FEATURES], ","), &res.Diagnostics) - } + state.fromEnv(ctx, appRes.EnvAsMap()) diags = res.State.Set(ctx, state) res.Diagnostics.Append(diags...) diff --git a/pkg/resources/rust/rust.go b/pkg/resources/rust/rust.go index e6d739d7..bdfcbe45 100644 --- a/pkg/resources/rust/rust.go +++ b/pkg/resources/rust/rust.go @@ -11,6 +11,13 @@ type ResourceRust struct { helper.Configurer } +const ( + CC_RUSTUP_CHANNEL = "CC_RUSTUP_CHANNEL" + CC_RUST_BIN = "CC_RUST_BIN" + CC_RUST_FEATURES = "CC_RUST_FEATURES" + CC_RUN_COMMAND = "CC_RUN_COMMAND" +) + func NewResourceRust() resource.Resource { return &ResourceRust{} } @@ -18,5 +25,3 @@ func NewResourceRust() resource.Resource { func (r *ResourceRust) Metadata(ctx context.Context, req resource.MetadataRequest, res *resource.MetadataResponse) { res.TypeName = req.ProviderTypeName + "_rust" } - -const CC_RUST_FEATURES = "CC_RUST_FEATURES" diff --git a/pkg/resources/rust/schema.go b/pkg/resources/rust/schema.go index 6b6a65eb..540b9202 100644 --- a/pkg/resources/rust/schema.go +++ b/pkg/resources/rust/schema.go @@ -13,11 +13,15 @@ import ( "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Rust struct { attributes.Runtime - Features types.Set `tfsdk:"features"` + Features types.Set `tfsdk:"features"` + RunCommand types.String `tfsdk:"run_command"` + RustBin types.String `tfsdk:"rust_bin"` + RustupChannel types.String `tfsdk:"rustup_channel"` } func (r Rust) FeaturesAsStrings(ctx context.Context, diags *diag.Diagnostics) []string { @@ -35,6 +39,7 @@ func (r ResourceRust) Schema(ctx context.Context, req resource.SchemaRequest, re Version: 1, MarkdownDescription: rustDoc, Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + // CC_RUST_FEATURES // https://doc.rust-lang.org/cargo/reference/features.html#command-line-feature-options // Multiple features may be separated with commas or spaces. // If using spaces, be sure to use quotes around all the features if running Cargo from a shell (such as --features "foo bar"). @@ -44,25 +49,31 @@ func (r ResourceRust) Schema(ctx context.Context, req resource.SchemaRequest, re Optional: true, MarkdownDescription: "List of Rust features to enable during build", }, + // CC_RUN_COMMAND + "run_command": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Custom command to run your application", + }, + // CC_RUST_BIN + "rust_bin": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The name of the binary to launch once built", + }, + // CC_RUSTUP_CHANNEL + "rustup_channel": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The rust channel to use. Use a specific channel version with `stable`, `beta`, `nightly` or a specific version like `1.13.0` (default: `stable`)", + }, }), Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), } } func (r Rust) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(r.Environment.ElementsAs(ctx, &customEnv, false)...) + env := r.ToEnv(ctx, diags) if diags.HasError() { return env } - env = pkg.Merge(env, customEnv) - - pkg.IfIsSetStr(r.AppFolder, func(s string) { env["APP_FOLDER"] = s }) - env = pkg.Merge(env, r.Hooks.ToEnv()) // Handle Rust features features := r.FeaturesAsStrings(ctx, diags) @@ -70,9 +81,29 @@ func (r Rust) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]str env[CC_RUST_FEATURES] = strings.Join(features, ",") } + pkg.IfIsSetStr(r.RunCommand, func(s string) { env[CC_RUN_COMMAND] = s }) + pkg.IfIsSetStr(r.RustBin, func(s string) { env[CC_RUST_BIN] = s }) + pkg.IfIsSetStr(r.RustupChannel, func(s string) { env[CC_RUSTUP_CHANNEL] = s }) + return env } +func (r *Rust) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + // Handle Rust features - convert comma-separated string to Set + if featuresStr := m.Pop(CC_RUST_FEATURES); featuresStr != "" { + features := strings.Split(featuresStr, ",") + r.Features, _ = types.SetValueFrom(ctx, types.StringType, features) + } + + r.RunCommand = pkg.FromStr(m.Pop(CC_RUN_COMMAND)) + r.RustBin = pkg.FromStr(m.Pop(CC_RUST_BIN)) + r.RustupChannel = pkg.FromStr(m.Pop(CC_RUSTUP_CHANNEL)) + + r.FromEnvironment(ctx, m) +} + func (r Rust) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if r.Deployment == nil || r.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/scala/crud.go b/pkg/resources/scala/crud.go index 180caaad..b72211a4 100644 --- a/pkg/resources/scala/crud.go +++ b/pkg/resources/scala/crud.go @@ -113,15 +113,7 @@ func (r *ResourceScala) Read(ctx context.Context, req resource.ReadRequest, resp state.BuildFlavor = readRes.GetBuildFlavor() state.VHosts = helper.VHostsFromAPIHosts(ctx, readRes.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) - - for envName, envValue := range readRes.EnvAsMap() { - switch envName { - case "APP_FOLDER": - state.AppFolder = pkg.FromStr(envValue) - default: - //state.Environment. - } - } + state.fromEnv(ctx, readRes.EnvAsMap()) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } diff --git a/pkg/resources/scala/scala.go b/pkg/resources/scala/scala.go index dad90be0..5dcc6129 100644 --- a/pkg/resources/scala/scala.go +++ b/pkg/resources/scala/scala.go @@ -10,6 +10,12 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +const ( + CC_SBT_TARGET_BIN = "CC_SBT_TARGET_BIN" + CC_SBT_TARGET_DIR = "CC_SBT_TARGET_DIR" + SBT_DEPLOY_GOAL = "SBT_DEPLOY_GOAL" +) + type ResourceScala struct { helper.Configurer } diff --git a/pkg/resources/scala/schema.go b/pkg/resources/scala/schema.go index 6a186ea1..64ad1e98 100644 --- a/pkg/resources/scala/schema.go +++ b/pkg/resources/scala/schema.go @@ -3,25 +3,27 @@ package scala import ( "context" _ "embed" - "maps" "github.com/go-git/go-git/v5/plumbing/transport/http" "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/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Scala struct { attributes.Runtime - // Scala related + SbtDeployGoal types.String `tfsdk:"sbt_deploy_goal"` + SbtTargetBin types.String `tfsdk:"sbt_target_bin"` + SbtTargetDir types.String `tfsdk:"sbt_target_dir"` } type ScalaV0 struct { attributes.RuntimeV0 - // Scala related } //go:embed doc.md @@ -34,8 +36,24 @@ func (r ResourceScala) Schema(ctx context.Context, req resource.SchemaRequest, r var schemaScala = schema.Schema{ Version: 1, MarkdownDescription: scalaDoc, - Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{}), - Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), + Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + // SBT_DEPLOY_GOAL + "sbt_deploy_goal": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define which SBT goals to run during build (default: `stage`)", + }, + // CC_SBT_TARGET_BIN + "sbt_target_bin": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define the bin to pick in the `CC_SBT_TARGET_DIR`", + }, + // CC_SBT_TARGET_DIR + "sbt_target_dir": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Define the folder the `target` dir is in (default: `.`)", + }, + }), + Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), } var schemaScalaV0 = schema.Schema{ @@ -46,21 +64,28 @@ var schemaScalaV0 = schema.Schema{ } func (plan *Scala) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(plan.Environment.ElementsAs(ctx, &customEnv, false)...) + env := plan.ToEnv(ctx, diags) if diags.HasError() { return env } - maps.Copy(env, customEnv) - pkg.IfIsSetStr(plan.AppFolder, func(s string) { env["APP_FOLDER"] = s }) + pkg.IfIsSetStr(plan.SbtDeployGoal, func(s string) { env[SBT_DEPLOY_GOAL] = s }) + pkg.IfIsSetStr(plan.SbtTargetBin, func(s string) { env[CC_SBT_TARGET_BIN] = s }) + pkg.IfIsSetStr(plan.SbtTargetDir, func(s string) { env[CC_SBT_TARGET_DIR] = s }) + return env } +func (scala *Scala) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + scala.SbtDeployGoal = pkg.FromStr(m.Pop(SBT_DEPLOY_GOAL)) + scala.SbtTargetBin = pkg.FromStr(m.Pop(CC_SBT_TARGET_BIN)) + scala.SbtTargetDir = pkg.FromStr(m.Pop(CC_SBT_TARGET_DIR)) + + scala.FromEnvironment(ctx, m) +} + func (java *Scala) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if java.Deployment == nil || java.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/static/crud.go b/pkg/resources/static/crud.go index 52bc8b5e..8981f520 100644 --- a/pkg/resources/static/crud.go +++ b/pkg/resources/static/crud.go @@ -111,15 +111,7 @@ func (r *ResourceStatic) Read(ctx context.Context, req resource.ReadRequest, res state.BuildFlavor = readRes.GetBuildFlavor() state.VHosts = helper.VHostsFromAPIHosts(ctx, readRes.App.Vhosts.AsString(), state.VHosts, &resp.Diagnostics) - - for envName, envValue := range readRes.EnvAsMap() { - switch envName { - case "APP_FOLDER": - state.AppFolder = pkg.FromStr(envValue) - default: - //state.Environment. - } - } + state.fromEnv(ctx, readRes.EnvAsMap()) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } diff --git a/pkg/resources/static/schema.go b/pkg/resources/static/schema.go index 64149b07..e0ac9ef6 100644 --- a/pkg/resources/static/schema.go +++ b/pkg/resources/static/schema.go @@ -3,24 +3,35 @@ package static import ( "context" _ "embed" + "fmt" + "strconv" "github.com/go-git/go-git/v5/plumbing/transport/http" "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/types" "go.clever-cloud.com/terraform-provider/pkg" "go.clever-cloud.com/terraform-provider/pkg/application" "go.clever-cloud.com/terraform-provider/pkg/attributes" + "go.clever-cloud.com/terraform-provider/pkg/helper" ) type Static struct { attributes.Runtime - // Static related + BuildCommand types.String `tfsdk:"build_command"` + HugoVersion types.String `tfsdk:"hugo_version"` + OverrideBuildcache types.String `tfsdk:"override_buildcache"` + StaticAutobuildOutDir types.String `tfsdk:"static_autobuild_outdir"` + StaticCaddyfile types.String `tfsdk:"static_caddyfile"` + StaticFlags types.String `tfsdk:"static_flags"` + StaticPort types.Int64 `tfsdk:"static_port"` + StaticServer types.String `tfsdk:"static_server"` + WebRoot types.String `tfsdk:"webroot"` } type StaticV0 struct { attributes.RuntimeV0 - // Static related } //go:embed doc.md @@ -33,8 +44,54 @@ func (r ResourceStatic) Schema(ctx context.Context, req resource.SchemaRequest, var schemaStatic = schema.Schema{ Version: 1, MarkdownDescription: staticDoc, - Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{}), - Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), + Attributes: attributes.WithRuntimeCommons(map[string]schema.Attribute{ + // CC_BUILD_COMMAND + "build_command": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Command to run during build phase", + }, + // CC_HUGO_VERSION + "hugo_version": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Set Hugo version (e.g., `0.150`)", + }, + // CC_OVERRIDE_BUILDCACHE + "override_buildcache": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Customize build cache directories", + }, + // CC_STATIC_AUTOBUILD_OUTDIR + "static_autobuild_outdir": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Output directory for static site generator (default: `/cc_static_autobuilt`)", + }, + // CC_STATIC_CADDYFILE + "static_caddyfile": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Path to Caddyfile for custom Caddy configuration (default: `./Caddyfile`)", + }, + // CC_STATIC_FLAGS + "static_flags": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Custom command line flags to pass to the static server", + }, + // CC_STATIC_PORT + "static_port": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Custom listen port for the static server (default: `8080`)", + }, + // CC_STATIC_SERVER + "static_server": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Server to use for static website (default: `static-web-server`)", + }, + // CC_WEBROOT + "webroot": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Path to web content to serve (default: `/`)", + }, + }), + Blocks: attributes.WithBlockRuntimeCommons(map[string]schema.Block{}), } var schemaStaticV0 = schema.Schema{ @@ -45,23 +102,44 @@ var schemaStaticV0 = schema.Schema{ } func (plan *Static) toEnv(ctx context.Context, diags *diag.Diagnostics) map[string]string { - env := map[string]string{} - - // do not use the real map since ElementAs can nullish it - // https://github.com/hashicorp/terraform-plugin-framework/issues/698 - customEnv := map[string]string{} - diags.Append(plan.Environment.ElementsAs(ctx, &customEnv, false)...) + env := plan.ToEnv(ctx, diags) if diags.HasError() { return env } - for k, v := range customEnv { - env[k] = v - } - pkg.IfIsSetStr(plan.AppFolder, func(s string) { env["APP_FOLDER"] = s }) + pkg.IfIsSetStr(plan.BuildCommand, func(s string) { env[CC_BUILD_COMMAND] = s }) + pkg.IfIsSetStr(plan.HugoVersion, func(s string) { env[CC_HUGO_VERSION] = s }) + pkg.IfIsSetStr(plan.OverrideBuildcache, func(s string) { env[CC_OVERRIDE_BUILDCACHE] = s }) + pkg.IfIsSetStr(plan.StaticAutobuildOutDir, func(s string) { env[CC_STATIC_AUTOBUILD_OUTDIR] = s }) + pkg.IfIsSetStr(plan.StaticCaddyfile, func(s string) { env[CC_STATIC_CADDYFILE] = s }) + pkg.IfIsSetStr(plan.StaticFlags, func(s string) { env[CC_STATIC_FLAGS] = s }) + pkg.IfIsSetI(plan.StaticPort, func(i int64) { env[CC_STATIC_PORT] = fmt.Sprintf("%d", i) }) + pkg.IfIsSetStr(plan.StaticServer, func(s string) { env[CC_STATIC_SERVER] = s }) + pkg.IfIsSetStr(plan.WebRoot, func(s string) { env[CC_WEBROOT] = s }) + return env } +func (static *Static) fromEnv(ctx context.Context, env map[string]string) { + m := helper.NewEnvMap(env) + + static.BuildCommand = pkg.FromStr(m.Pop(CC_BUILD_COMMAND)) + static.HugoVersion = pkg.FromStr(m.Pop(CC_HUGO_VERSION)) + static.OverrideBuildcache = pkg.FromStr(m.Pop(CC_OVERRIDE_BUILDCACHE)) + static.StaticAutobuildOutDir = pkg.FromStr(m.Pop(CC_STATIC_AUTOBUILD_OUTDIR)) + static.StaticCaddyfile = pkg.FromStr(m.Pop(CC_STATIC_CADDYFILE)) + static.StaticFlags = pkg.FromStr(m.Pop(CC_STATIC_FLAGS)) + + if port, err := strconv.ParseInt(m.Pop(CC_STATIC_PORT), 10, 64); err == nil { + static.StaticPort = pkg.FromI(port) + } + + static.StaticServer = pkg.FromStr(m.Pop(CC_STATIC_SERVER)) + static.WebRoot = pkg.FromStr(m.Pop(CC_WEBROOT)) + + static.FromEnvironment(ctx, m) +} + func (java *Static) toDeployment(gitAuth *http.BasicAuth) *application.Deployment { if java.Deployment == nil || java.Deployment.Repository.IsNull() { return nil diff --git a/pkg/resources/static/static.go b/pkg/resources/static/static.go index c096bae9..245d9f85 100644 --- a/pkg/resources/static/static.go +++ b/pkg/resources/static/static.go @@ -10,6 +10,18 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +const ( + CC_BUILD_COMMAND = "CC_BUILD_COMMAND" + CC_HUGO_VERSION = "CC_HUGO_VERSION" + CC_OVERRIDE_BUILDCACHE = "CC_OVERRIDE_BUILDCACHE" + CC_STATIC_AUTOBUILD_OUTDIR = "CC_STATIC_AUTOBUILD_OUTDIR" + CC_STATIC_CADDYFILE = "CC_STATIC_CADDYFILE" + CC_STATIC_FLAGS = "CC_STATIC_FLAGS" + CC_STATIC_PORT = "CC_STATIC_PORT" + CC_STATIC_SERVER = "CC_STATIC_SERVER" + CC_WEBROOT = "CC_WEBROOT" +) + type ResourceStatic struct { helper.Configurer }