Repository: kreuzwerker/terraform-provider-docker

Issue:
Support for Docker contexts
<!--- 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 --->

### Description

Look up the Docker host from [Docker Context](https://docs.docker.com/engine/context/working-with-contexts/) instead of `DOCKER_HOST` or the hardcoded `/var/run/docker.sock`. A nice bonus would be to select a context with an argument to the provider.

### Potential Terraform Configuration

<!--- Information about code formatting: https://help.github.com/articles/basic-writing-and-formatting-syntax/#quoting-code --->

```hcl
provider "docker" {
  context = "development"
}
```

In my particular case I want to the provider to work with https://github.com/abiosoft/colima. For a workaround I do `export DOCKER_HOST=$(docker context inspect | jq -r '.[0].Endpoints.docker.Host')`.

Hints from issue comments:
@matifali: Yes, it should make life easy. Currently, I am using custom logic to extract the host from the selected docker context. @nathanblair: Does anyone know if there is a Docker API call with the Docker context information?

I've got a fork with a fix that _could_ work but it just relies on testing the presence of a couple possible `docker.sock` paths. So not very determinable.@matifali: docker context reads from the local `~/.docker/contexts/` directory so it does not have an API.
also, this is different for each of the logged-in users. @cryptiklemur: This could potentially solve https://github.com/kreuzwerker/terraform-provider-docker/issues/262 as well

Solution patch:
diff --git a/docs/index.md b/docs/index.md
index 1ee5cfc..f59b72b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -154,6 +154,7 @@ provider "docker" {
 - `ca_material` (String) PEM-encoded content of Docker host CA certificate
 - `cert_material` (String) PEM-encoded content of Docker client certificate
 - `cert_path` (String) Path to directory with Docker TLS config
+- `context` (String) The name of the Docker context to use. Can also be set via `DOCKER_CONTEXT` environment variable. Overrides the `host` if set.
 - `disable_docker_daemon_check` (Boolean) If set to `true`, the provider will not check if the Docker daemon is running. This is useful for resources/data_sourcess that do not require a running Docker daemon, such as the data source `docker_registry_image`.
 - `host` (String) The Docker daemon address
 - `key_material` (String) PEM-encoded content of Docker client private key
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 80e2d3c..fa885af 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 	"log"
@@ -53,6 +54,12 @@ func New(version string) func() *schema.Provider {
 					},
 					Description: "The Docker daemon address",
 				},
+				"context": {
+					Type:        schema.TypeString,
+					Optional:    true,
+					DefaultFunc: schema.EnvDefaultFunc("DOCKER_CONTEXT", ""),
+					Description: "The name of the Docker context to use. Can also be set via `DOCKER_CONTEXT` environment variable. Overrides the `host` if set.",
+				},
 				"ssh_opts": {
 					Type:     schema.TypeList,
 					Optional: true,
@@ -178,13 +185,29 @@ func New(version string) func() *schema.Provider {
 
 func configure(version string, p *schema.Provider) func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) {
 	return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
+		var host string
+		if contextName := d.Get("context").(string); contextName != "" {
+			usr, err := user.Current()
+			if err != nil {
+				return nil, diag.Errorf("Could not determine current user. We don't know what the homedir is to look for docker contexts: %v", err)
+			}
+			log.Printf("[DEBUG] Homedir %s", usr.HomeDir)
+			contextHost, err := getContextHost(contextName, usr.HomeDir)
+			if err != nil {
+				return nil, diag.Errorf("Error loading Docker context '%s': %s", contextName, err)
+			}
+			host = contextHost
+		} else {
+			host = d.Get("host").(string)
+		}
+
 		SSHOptsI := d.Get("ssh_opts").([]interface{})
 		SSHOpts := make([]string, len(SSHOptsI))
 		for i, s := range SSHOptsI {
 			SSHOpts[i] = s.(string)
 		}
 		config := Config{
-			Host:     d.Get("host").(string),
+			Host:     host,
 			SSHOpts:  SSHOpts,
 			Ca:       d.Get("ca_material").(string),
 			Cert:     d.Get("cert_material").(string),
@@ -229,6 +252,45 @@ func configure(version string, p *schema.Provider) func(context.Context, *schema
 	}
 }
 
+func getContextHost(contextName string, homedir string) (string, error) {
+	contextsDir := fmt.Sprintf("%s/.docker/contexts/meta", homedir)
+	files, err := os.ReadDir(contextsDir)
+	if err != nil {
+		return "", fmt.Errorf("could not read contexts directory: %v", err)
+	}
+
+	for _, file := range files {
+		metaFilePath := fmt.Sprintf("%s/%s/meta.json", contextsDir, file.Name())
+		metaFile, err := os.Open(metaFilePath)
+		if err != nil {
+			log.Printf("[DEBUG] Skipping file %s due to error: %v", metaFilePath, err)
+			continue
+		}
+
+		var meta struct {
+			Name      string `json:"Name"`
+			Endpoints map[string]struct {
+				Host string `json:"Host"`
+			} `json:"Endpoints"`
+		}
+		err = json.NewDecoder(metaFile).Decode(&meta)
+		// Ensure the file is closed immediately after reading
+		metaFile.Close() // nolint:errcheck
+		if err != nil {
+			log.Printf("[DEBUG] Skipping file %s due to JSON parsing error: %v", metaFilePath, err)
+			continue
+		}
+
+		if meta.Name == contextName {
+			if endpoint, ok := meta.Endpoints["docker"]; ok {
+				return endpoint.Host, nil
+			}
+		}
+	}
+
+	return "", fmt.Errorf("context '%s' not found", contextName)
+}
+
 // AuthConfigs represents authentication options to use for the
 // PushImage method accommodating the new X-Registry-Config header
 type AuthConfigs struct {



Test patch:
diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go
index c64a238..d3a0df9 100644
--- a/internal/provider/provider_test.go
+++ b/internal/provider/provider_test.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"context"
 	"fmt"
+	"os"
 	"os/exec"
 	"regexp"
 	"testing"
@@ -110,6 +111,54 @@ func testAccPreCheck(t *testing.T) {
 	}
 }
 
+func TestGetContextHost_ValidContext(t *testing.T) {
+	// Create a temporary directory to simulate Docker contexts
+	tempDir := t.TempDir()
+	contextName := "test-context"
+	contextUUID := "1234-5678-91011"
+	contextFilePath := fmt.Sprintf("%s/.docker/contexts/meta/%s/meta.json", tempDir, contextUUID)
+
+	// Simulate a valid Docker context file
+	contextData := `{
+		"Name": "test-context",
+		"Endpoints": {
+			"docker": {
+				"Host": "tcp://docker:2375"
+			}
+		}
+	}`
+	if err := os.MkdirAll(fmt.Sprintf("%s/.docker/contexts/meta/%s", tempDir, contextUUID), 0755); err != nil {
+		t.Fatalf("Failed to create context directory: %s", err)
+	}
+	if err := os.WriteFile(contextFilePath, []byte(contextData), 0644); err != nil {
+		t.Fatalf("Failed to write context file: %s", err)
+	}
+
+	// Test the function
+	host, err := getContextHost(contextName, tempDir)
+	if err != nil {
+		t.Fatalf("Expected no error, got: %s", err)
+	}
+	if host != "tcp://docker:2375" {
+		t.Fatalf("Expected host 'tcp://docker:2375', got: %s", host)
+	}
+}
+
+func TestGetContextHost_InvalidContext(t *testing.T) {
+	// Create a temporary directory to simulate Docker contexts
+	tempDir := t.TempDir()
+
+	if err := os.MkdirAll(fmt.Sprintf("%s/.docker/contexts/meta/foobar", tempDir), 0755); err != nil {
+		t.Fatalf("Failed to create context directory: %s", err)
+	}
+
+	// Test the function with a non-existent context
+	_, err := getContextHost("non-existent-context", tempDir)
+	if err == nil || err.Error() != "context 'non-existent-context' not found" {
+		t.Fatalf("Expected error 'context 'non-existent-context' not found', got: %v", err)
+	}
+}
+
 const testAccDockerProviderWithIncompleteAuthConfig = `
 provider "docker" {
 	alias = "private"