diff --git a/docs/index.md b/docs/index.md index 1ee5cfc0..f59b72b4 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 80e2d3c8..fa885afe 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 { diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index c64a2387..d3a0df96 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"