Repository: kreuzwerker/terraform-provider-docker

Issue:
docker_container.cpus causes panic when assigned string value
<!---
Please note the following potential times when an issue might be in Terraform core:

* [Configuration Language](https://www.terraform.io/docs/configuration/index.html) or resource ordering issues
* [State](https://www.terraform.io/docs/state/index.html) and [State Backend](https://www.terraform.io/docs/backends/index.html) issues
* [Provisioner](https://www.terraform.io/docs/provisioners/index.html) issues
* [Registry](https://registry.terraform.io/) issues
* Spans resources across multiple providers

If you are running into one of these scenarios, we recommend opening an issue in the [Terraform core repository](https://github.com/hashicorp/terraform/) instead.
--->

<!--- Please keep this note for the community --->

### Community Note

* Please vote on this issue by adding a 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to the original issue to help the community and maintainers prioritize this request
* Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
* If you are interested in working on this issue or have submitted a pull request, please leave a comment

<!--- Thank you for keeping this note for the community --->

### Terraform (and docker Provider) Version

```text
Terraform v1.12.2
on linux_amd64
+ provider registry.terraform.io/kreuzwerker/docker v3.6.2
```

<!--- Please run `terraform -v` to show the Terraform core version and provider version(s). If you are not running the latest version of Terraform or the provider, please upgrade because your issue may have already been fixed. [Terraform documentation on provider versioning](https://www.terraform.io/docs/configuration/providers.html#provider-versions). --->

### Affected Resource(s)

<!--- Please list the affected resources and data sources. --->

`docker_container`

### Terraform Configuration Files

<!--- Information about code formatting: https://help.github.com/articles/basic-writing-and-formatting-syntax/#quoting-code --->


```hcl
resource "docker_container" "nginx" {
  image  = docker_image.nginx.image_id
  name   = "nginx"
  cpus   = "1.5"
  memory = 1024
}
```

### Debug Output

<!---
Please provide a link to a GitHub Gist containing the complete debug output. Please do NOT paste the debug output in the issue; just paste a link to the Gist.

To obtain the debug output, see the [Terraform documentation on debugging](https://www.terraform.io/docs/internals/debugging.html).
--->

### Panic Output

<!--- If Terraform produced a panic, please provide a link to a GitHub Gist containing the output of the `crash.log`. --->

```
│ Error: Plugin did not respond
│ 
│   with docker_container.nginx,
│   on main.tf line 17, in resource "docker_container" "nginx":
│   17: resource "docker_container" "nginx" {
│ 
│ The plugin encountered an error, and failed to respond to the plugin.(*GRPCProvider).ApplyResourceChange call. The plugin logs may contain more details.
╵
```

```
Stack trace from the terraform-provider-docker_v3.6.2 plugin:

panic: interface conversion: interface {} is string, not float32

goroutine 32 [running]:
github.com/terraform-providers/terraform-provider-docker/internal/provider.resourceDockerContainerCreate({0x2d1dbf0, 0xc000370310}, 0xc0000fc240, {0x22eb160, 0xc0007325c0})
        github.com/terraform-providers/terraform-provider-docker/internal/provider/resource_docker_container_funcs.go:328 +0x4796
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).create(0xc000272500, {0x2d1db48, 0xc00092e180}, 0xc0000fc240, {0x22eb160, 0xc0007325c0})
        github.com/hashicorp/terraform-plugin-sdk/v2@v2.37.0/helper/schema/resource.go:849 +0x119
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).Apply(0xc000272500, {0x2d1db48, 0xc00092e180}, 0xc000932750, 0xc000859d40, {0x22eb160, 0xc0007325c0})
        github.com/hashicorp/terraform-plugin-sdk/v2@v2.37.0/helper/schema/resource.go:980 +0xb47
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*GRPCProviderServer).ApplyResourceChange(0xc0005dcf48, {0x2d1db48?, 0xc00092e0c0?}, 0xc0008b1680)
        github.com/hashicorp/terraform-plugin-sdk/v2@v2.37.0/helper/schema/grpc_provider.go:1484 +0x1085
github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server.(*server).ApplyResourceChange(0xc0005e6000, {0x2d1db48?, 0xc000913560?}, 0xc00091e880)
        github.com/hashicorp/terraform-plugin-go@v0.27.0/tfprotov5/tf5server/server.go:989 +0x3b9
github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5._Provider_ApplyResourceChange_Handler({0x28aa860, 0xc0005e6000}, {0x2d1db48, 0xc000913560}, 0xc00091e800, 0x0)
        github.com/hashicorp/terraform-plugin-go@v0.27.0/tfprotov5/internal/tfplugin5/tfplugin5_grpc.pb.go:687 +0x1a6
google.golang.org/grpc.(*Server).processUnaryRPC(0xc000012200, {0x2d1db48, 0xc0009134d0}, 0xc0002cbb00, 0xc000646420, 0x4337448, 0x0)
        google.golang.org/grpc@v1.72.1/server.go:1405 +0x1036
google.golang.org/grpc.(*Server).handleStream(0xc000012200, {0x2d1f020, 0xc0006d0000}, 0xc0002cbb00)
        google.golang.org/grpc@v1.72.1/server.go:1815 +0xb88
google.golang.org/grpc.(*Server).serveStreams.func2.1()
        google.golang.org/grpc@v1.72.1/server.go:1035 +0x7f
created by google.golang.org/grpc.(*Server).serveStreams.func2 in goroutine 43
        google.golang.org/grpc@v1.72.1/server.go:1046 +0x11d

Error: The terraform-provider-docker_v3.6.2 plugin crashed!
```

### Expected Behaviour

<!--- What should have happened? --->

### Actual Behaviour

<!--- What actually happened? --->

### Steps to Reproduce

<!--- Please list the steps required to reproduce the issue. --->

1. `terraform apply`

### Important Factoids

<!--- Are there anything atypical about your setup that we should know? For example: Running the docker daemon with TLS? --->

### References

<!---
Information about referencing Github Issues: https://help.github.com/articles/basic-writing-and-formatting-syntax/#referencing-issues-and-pull-requests

Are there any other GitHub issues (open or closed) or pull requests that should be linked here? Such as vendor documentation?
--->

* #0000


Discussion:
@Syakyr: I have a similar issue to this as well, and on my end, it seems to be a regression.

## Context

- I'm using this plugin to spin up a [Coder workspace as Docker containers from a Terraform template](https://coder.com/docs/admin/templates/creating-templates).
- Issue results in error if and only if `docker_container.cpus` is specified, whether the value is `number`, `float` or `string`.

## Terraform (and Docker provider) version

- Tested on Terraform version 1.11.4 and 1.12.2
- Affects Docker provider version 3.6.x, issue not affecting version 3.5.0

## Affected Resource

`docker_container`

## Debug/Panic Output 

Main line to look out for is:
```
panic: interface conversion: interface {} is string, not float32
```

<details><summary>Click for logs (for Terraform 1.12.2):</summary>

```
Terraform 1.12.2
docker_container.workspace[0]: Plan to replace
docker_container.workspace[0]: Destroying... [id=6d9b6603750570745f9d6ddb35022e73cede3f75709483b68a4a15bb318c6704]
docker_container.workspace[0]: Destruction complete after 1s
docker_container.workspace[0]: Creating...
docker_container.workspace[0]: Creation errored after 0s
Error: Request cancelled
on main.tf line 356, in resource "docker_container" "workspace":
  356: resource "docker_container" "workspace" {
The plugin.(*GRPCProvider).ApplyResourceChange request was cancelled.
Stack trace from the terraform-provider-docker_v3.6.2 plugin:
panic: interface conversion: interface {} is string, not float32
goroutine 44 [running]:
github.com/terraform-providers/terraform-provider-docker/internal/provider.resourceDockerContainerCreate({0x2d1dbf0, 0xc000434700}, 0xc000940fc0, {0x22eb160, 0xc00068e9c0})
	github.com/terraform-providers/terraform-provider-docker/internal/provider/resource_docker_container_funcs.go:328 +0x4796
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).create(0xc0001da500, {0x2d1db48, 0xc00080b3e0}, 0xc000940fc0, {0x22eb160, 0xc00068e9c0})
	github.com/hashicorp/terraform-plugin-sdk/v2@v2.37.0/helper/schema/resource.go:849 +0x119
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*Resource).Apply(0xc0001da500, {0x2d1db48, 0xc00080b3e0}, 0xc000a916c0, 0xc000940ea0, {0x22eb160, 0xc00068e9c0})
	github.com/hashicorp/terraform-plugin-sdk/v2@v2.37.0/helper/schema/resource.go:980 +0xb47
github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema.(*GRPCProviderServer).ApplyResourceChange(0xc00055a7f8, {0x2d1db48?, 0xc00080b2f0?}, 0xc00017caf0)
	github.com/hashicorp/terraform-plugin-sdk/v2@v2.37.0/helper/schema/grpc_provider.go:1484 +0x1085
github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server.(*server).ApplyResourceChange(0xc00034bea0, {0x2d1db48?, 0xc00080a570?}, 0xc000b1c200)
	github.com/hashicorp/terraform-plugin-go@v0.27.0/tfprotov5/tf5server/server.go:989 +0x3b9
github.com/hashicorp/terraform-plugin-go/tfprotov5/internal/tfplugin5._Provider_ApplyResourceChange_Handler({0x28aa860, 0xc00034bea0}, {0x2d1db48, 0xc00080a570}, 0xc000b1c180, 0x0)
	github.com/hashicorp/terraform-plugin-go@v0.27.0/tfprotov5/internal/tfplugin5/tfplugin5_grpc.pb.go:687 +0x1a6
google.golang.org/grpc.(*Server).processUnaryRPC(0xc000236000, {0x2d1db48, 0xc00080a210}, 0xc0002184e0, 0xc0006ede00, 0x4337448, 0x0)
	google.golang.org/grpc@v1.72.1/server.go:1405 +0x1036
google.golang.org/grpc.(*Server).handleStream(0xc000236000, {0x2d1f020, 0xc0007b2000}, 0xc0002184e0)
	google.golang.org/grpc@v1.72.1/server.go:1815 +0xb88
google.golang.org/grpc.(*Server).serveStreams.func2.1()
	google.golang.org/grpc@v1.72.1/server.go:1035 +0x7f
created by google.golang.org/grpc.(*Server).serveStreams.func2 in goroutine 42
	google.golang.org/grpc@v1.72.1/server.go:1046 +0x11d
Error: The terraform-provider-docker_v3.6.2 plugin crashed!
This is always indicative of a bug within the plugin. It would be immensely
helpful if you could report the crash with the plugin's maintainers so that it
can be fixed. The output above should help diagnose the issue.
```
@Junkern: Thanks for reporting! I am preparing a fix for this and will be released in the next couple of days

Solution patch:
diff --git a/.github/workflows/compile.yaml b/.github/workflows/compile.yaml
index 4107a72..0c3bf68 100644
--- a/.github/workflows/compile.yaml
+++ b/.github/workflows/compile.yaml
@@ -19,8 +19,23 @@ env:
   GO_VERSION: "1.22"
 
 jobs:
+  compile-fast:
+    runs-on: ubuntu-22.04
+    if: github.event.pull_request.draft == true
+    steps:
+      - uses: actions/checkout@v5
+      - uses: actions/setup-go@v5
+        with:
+          go-version: ${{ env.GO_VERSION }}
+      - name: Run GoReleaser (Fast)
+        uses: goreleaser/goreleaser-action@v5
+        with:
+          version: latest
+          args: release --config .goreleaser-fast.yml --rm-dist --skip-publish --snapshot --skip-sign
+
   compile:
     runs-on: ubuntu-22.04
+    if: github.event.pull_request.draft != true
     steps:
       - uses: actions/checkout@v5
       - uses: actions/setup-go@v5
diff --git a/.goreleaser-fast.yml b/.goreleaser-fast.yml
new file mode 100644
index 0000000..fa18627
--- /dev/null
+++ b/.goreleaser-fast.yml
@@ -0,0 +1,27 @@
+# Fast build configuration for development/PR builds
+# Only builds for the most common platforms to provide quick feedback
+builds:
+  - env:
+      - CGO_ENABLED=0
+    mod_timestamp: "{{ .CommitTimestamp }}"
+    flags:
+      - -trimpath
+    ldflags:
+      - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}"
+    goos:
+      - linux
+      - darwin
+    goarch:
+      - amd64
+      - arm64
+    binary: "{{ .ProjectName }}_v{{ .Version }}"
+archives:
+  - format: zip
+    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
+checksum:
+  name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS"
+  algorithm: sha256
+release:
+  disable: true
+changelog:
+  skip: true
diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go
index 1ae864a..b2ec9b0 100644
--- a/internal/provider/helpers.go
+++ b/internal/provider/helpers.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"fmt"
+	"math/big"
 	"strings"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
@@ -134,3 +135,23 @@ func containsIgnorableErrorMessage(errorMsg string, ignorableErrorMessages ...st
 
 	return false
 }
+
+// Convert nanoseconds to seconds as a decimal string
+// e.g., 1,000,000,000 nanoseconds = 1.0 seconds
+func nanoInt64ToDecimalString(nanoInt64 int64) string {
+	if nanoInt64 == 0 {
+		return "0"
+	}
+
+	rat := new(big.Rat).SetFrac64(nanoInt64, 1e9)
+	str := rat.FloatString(9)
+
+	// If we have a "round" value like 100000000, we want to return "1.0" instead of further processing
+	if strings.Count(str, "0") == 9 {
+		return rat.FloatString(1)
+	}
+
+	// Remove trailing zeros to ensure a clean representation without unnecessary decimal places
+	str = strings.TrimRight(str, "0")
+	return str
+}
diff --git a/internal/provider/resource_docker_container_funcs.go b/internal/provider/resource_docker_container_funcs.go
index e828fb3..809ac21 100644
--- a/internal/provider/resource_docker_container_funcs.go
+++ b/internal/provider/resource_docker_container_funcs.go
@@ -324,8 +324,12 @@ func resourceDockerContainerCreate(ctx context.Context, d *schema.ResourceData,
 	}
 
 	if v, ok := d.GetOk("cpus"); ok {
-		hostConfig.CPUPeriod = 100000
-		hostConfig.CPUQuota = int64(v.(float32) * 100000)
+		nanocpus, err := opts.ParseCPUs(v.(string))
+		if err != nil {
+			return diag.Errorf("Error setting cpus: %s", err)
+		}
+
+		hostConfig.NanoCPUs = nanocpus
 
 		if _, ook := d.GetOk("cpu_period"); ook {
 			log.Printf("[WARN] Value for cpus is set, ignore cpu_period setting")
@@ -786,7 +790,7 @@ func resourceDockerContainerRead(ctx context.Context, d *schema.ResourceData, me
 	}
 	d.Set("shm_size", container.HostConfig.ShmSize/1024/1024)
 	if container.HostConfig.NanoCPUs > 0 {
-		d.Set("cpus", container.HostConfig.NanoCPUs)
+		d.Set("cpus", nanoInt64ToDecimalString(container.HostConfig.NanoCPUs))
 	}
 	d.Set("cpu_shares", container.HostConfig.CPUShares)
 	d.Set("cpu_set", container.HostConfig.CpusetCpus)



Test patch:
diff --git a/internal/provider/helpers_test.go b/internal/provider/helpers_test.go
new file mode 100644
index 0000000..5783742
--- /dev/null
+++ b/internal/provider/helpers_test.go
@@ -0,0 +1,23 @@
+package provider
+
+import "testing"
+
+func TestNanoInt64ToDecimalString(t *testing.T) {
+	tests := []struct {
+		nanoInt64 int64
+		expected  string
+	}{
+		{0, "0"},
+		{1000000000, "1.0"},
+		{1500000000, "1.5"},
+		{2000000000, "2.0"},
+		{2250000000, "2.25"},
+	}
+
+	for _, test := range tests {
+		result := nanoInt64ToDecimalString(test.nanoInt64)
+		if result != test.expected {
+			t.Errorf("Expected %s for %d, got %s", test.expected, test.nanoInt64, result)
+		}
+	}
+}
diff --git a/internal/provider/resource_docker_container_test.go b/internal/provider/resource_docker_container_test.go
index 6c2903a..c32e62d 100644
--- a/internal/provider/resource_docker_container_test.go
+++ b/internal/provider/resource_docker_container_test.go
@@ -1450,6 +1450,23 @@ func TestAccDockerContainer_nameattrnochange(t *testing.T) {
 		},
 	})
 }
+
+func TestAccDockerContainer_cpus(t *testing.T) {
+	var c container.InspectResponse
+	resource.Test(t, resource.TestCase{
+		PreCheck:          func() { testAccPreCheck(t) },
+		ProviderFactories: providerFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: loadTestConfiguration(t, RESOURCE, "docker_container", "testAccDockerContainerCpus"),
+				Check: resource.ComposeTestCheckFunc(
+					testAccContainerRunning("docker_container.foo", &c),
+				),
+			},
+		},
+	})
+}
+
 func TestAccDockerContainer_nostart(t *testing.T) {
 	var c container.InspectResponse
 	resource.Test(t, resource.TestCase{
diff --git a/testdata/resources/docker_container/testAccDockerContainerCpus.tf b/testdata/resources/docker_container/testAccDockerContainerCpus.tf
new file mode 100644
index 0000000..27441cd
--- /dev/null
+++ b/testdata/resources/docker_container/testAccDockerContainerCpus.tf
@@ -0,0 +1,9 @@
+resource "docker_image" "foo" {
+  name = "nginx:latest"
+}
+
+resource "docker_container" "foo" {
+  image  = docker_image.foo.image_id
+  name   = "nginx"
+  cpus   = "1.5"
+}