terraform-provider-docker/internal/provider/resource_docker_registry_image_funcs.go
Martin 70852379ec
Some checks failed
Acc Tests / acc-test (TestAccDockerConfig, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerConfig, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerNetwork, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerNetwork, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerPlugin, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerPlugin, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerSecret, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerSecret, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerTag, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerTag, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerVolume, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (TestAccDockerVolume, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerContainer, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerContainer, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerImage, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerImage, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerRegistryImage, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerRegistryImage, 1.8.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerService, 0.15.x) (push) Has been cancelled
Acc Tests / acc-test (true, TestAccDockerService, 1.8.x) (push) Has been cancelled
Compile Binaries / compile-fast (push) Has been cancelled
Compile Binaries / compile (push) Has been cancelled
golangci-lint / lint (push) Has been cancelled
Unit Tests / unit-test (push) Has been cancelled
Website Checks / markdown-link-check (push) Has been cancelled
Docs and Website Lint / website-generation (push) Has been cancelled
Docs and Website Lint / website-lint-spellcheck-tffmt (push) Has been cancelled
Docs and Website Lint / markdown-lint (push) Has been cancelled
feat: Implement caching of docker provider (#808)
2025-10-16 20:18:34 +02:00

379 lines
14 KiB
Go

package provider
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/docker/go-units"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func buildAuthConfigFromResource(v interface{}) registry.AuthConfig {
auth := v.([]interface{})[0].(map[string]interface{})
return registry.AuthConfig{
ServerAddress: normalizeRegistryAddress(auth["address"].(string)),
Username: auth["username"].(string),
Password: auth["password"].(string),
}
}
func resourceDockerRegistryImageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, err := meta.(*ProviderConfig).MakeClient(ctx, d)
if err != nil {
return diag.Errorf("failed to create Docker client: %v", err)
}
providerConfig := meta.(*ProviderConfig)
name := d.Get("name").(string)
log.Printf("[DEBUG] Creating docker image %s", name)
if value, ok := d.GetOk("build"); ok {
for _, rawBuild := range value.(*schema.Set).List() {
shouldReturn, d1 := buildImage(ctx, rawBuild, client, name)
if shouldReturn {
return d1
}
}
}
pushOpts := createPushImageOptions(name)
var authConfig registry.AuthConfig
if v, ok := d.GetOk("auth_config"); ok {
log.Printf("[INFO] Using auth config from resource: %s", v)
authConfig = buildAuthConfigFromResource(v)
} else {
var err error
authConfig, err = getAuthConfigForRegistry(pushOpts.Registry, providerConfig)
log.Printf("[INFO] Using auth config from provider: %#v", authConfig)
if err != nil {
return diag.Errorf("resourceDockerRegistryImageCreate: Unable to get authConfig for registry: %s", err)
}
}
if err := pushDockerRegistryImage(ctx, client, pushOpts, authConfig.Username, authConfig.Password); err != nil {
return diag.Errorf("Error pushing docker image: %s", err)
}
insecureSkipVerify := d.Get("insecure_skip_verify").(bool)
digest, err := getImageDigestWithFallback(pushOpts, authConfig.ServerAddress, authConfig.Username, authConfig.Password, insecureSkipVerify)
if err != nil {
return diag.Errorf("Got error getting registry image digest inside resourceDockerRegistryImageCreate: %s", err)
}
d.SetId(digest)
d.Set("sha256_digest", digest)
return nil
}
func resourceDockerRegistryImageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
providerConfig := meta.(*ProviderConfig)
name := d.Get("name").(string)
pushOpts := createPushImageOptions(name)
var authConfig registry.AuthConfig
if v, ok := d.GetOk("auth_config"); ok {
authConfig = buildAuthConfigFromResource(v)
} else {
var err error
authConfig, err = getAuthConfigForRegistry(pushOpts.Registry, providerConfig)
if err != nil {
return diag.Errorf("resourceDockerRegistryImageRead: Unable to get authConfig for registry: %s", err)
}
}
insecureSkipVerify := d.Get("insecure_skip_verify").(bool)
digest, err := getImageDigestWithFallback(pushOpts, authConfig.ServerAddress, authConfig.Username, authConfig.Password, insecureSkipVerify)
if err != nil {
log.Printf("Got error getting registry image digest: %s", err)
d.SetId("")
return nil
}
d.Set("sha256_digest", digest)
return nil
}
func resourceDockerRegistryImageDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if d.Get("keep_remotely").(bool) {
return nil
}
providerConfig := meta.(*ProviderConfig)
name := d.Get("name").(string)
pushOpts := createPushImageOptions(name)
var authConfig registry.AuthConfig
if v, ok := d.GetOk("auth_config"); ok {
authConfig = buildAuthConfigFromResource(v)
} else {
var err error
authConfig, err = getAuthConfigForRegistry(pushOpts.Registry, providerConfig)
if err != nil {
return diag.Errorf("resourceDockerRegistryImageDelete: Unable to get authConfig for registry: %s", err)
}
}
digest := d.Get("sha256_digest").(string)
err := deleteDockerRegistryImage(pushOpts, authConfig.ServerAddress, digest, authConfig.Username, authConfig.Password, true, false)
if err != nil {
err = deleteDockerRegistryImage(pushOpts, authConfig.ServerAddress, pushOpts.Tag, authConfig.Username, authConfig.Password, true, true)
if err != nil {
return diag.Errorf("Got error deleting registry image: %s", err)
}
}
return nil
}
func resourceDockerRegistryImageUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return resourceDockerRegistryImageRead(ctx, d, meta)
}
// Helpers
type internalPushImageOptions struct {
Name string
FqName string
Registry string
NormalizedRegistry string
Repository string
Tag string
}
func createImageBuildOptions(buildOptions map[string]interface{}) build.ImageBuildOptions {
mapOfInterfacesToMapOfStrings := func(mapOfInterfaces map[string]interface{}) map[string]string {
mapOfStrings := make(map[string]string, len(mapOfInterfaces))
for k, v := range mapOfInterfaces {
mapOfStrings[k] = fmt.Sprintf("%v", v)
}
return mapOfStrings
}
mapToBuildArgs := func(buildArgsOptions map[string]interface{}) map[string]*string {
buildArgs := make(map[string]*string, len(buildArgsOptions))
for k, v := range buildArgsOptions {
value := v.(string)
buildArgs[k] = &value
}
return buildArgs
}
readULimits := func(options []interface{}) []*units.Ulimit {
ulimits := make([]*units.Ulimit, len(options))
for i, v := range options {
ulimitOption := v.(map[string]interface{})
ulimit := units.Ulimit{
Name: ulimitOption["name"].(string),
Hard: int64(ulimitOption["hard"].(int)),
Soft: int64(ulimitOption["soft"].(int)),
}
ulimits[i] = &ulimit
}
return ulimits
}
readAuthConfigs := func(options []interface{}) map[string]registry.AuthConfig {
authConfigs := make(map[string]registry.AuthConfig, len(options))
for _, v := range options {
authOptions := v.(map[string]interface{})
auth := registry.AuthConfig{
Username: authOptions["user_name"].(string),
Password: authOptions["password"].(string),
Auth: authOptions["auth"].(string),
Email: authOptions["email"].(string),
ServerAddress: authOptions["server_address"].(string),
IdentityToken: authOptions["identity_token"].(string),
RegistryToken: authOptions["registry_token"].(string),
}
authConfigs[authOptions["host_name"].(string)] = auth
}
return authConfigs
}
buildImageOptions := build.ImageBuildOptions{}
buildImageOptions.SuppressOutput = buildOptions["suppress_output"].(bool)
buildImageOptions.RemoteContext = buildOptions["remote_context"].(string)
buildImageOptions.NoCache = buildOptions["no_cache"].(bool)
buildImageOptions.Remove = buildOptions["remove"].(bool)
buildImageOptions.ForceRemove = buildOptions["force_remove"].(bool)
buildImageOptions.PullParent = buildOptions["pull_parent"].(bool)
buildImageOptions.Isolation = container.Isolation(buildOptions["isolation"].(string))
buildImageOptions.CPUSetCPUs = buildOptions["cpu_set_cpus"].(string)
buildImageOptions.CPUSetMems = buildOptions["cpu_set_mems"].(string)
buildImageOptions.CPUShares = int64(buildOptions["cpu_shares"].(int))
buildImageOptions.CPUQuota = int64(buildOptions["cpu_quota"].(int))
buildImageOptions.CPUPeriod = int64(buildOptions["cpu_period"].(int))
buildImageOptions.Memory = int64(buildOptions["memory"].(int))
buildImageOptions.MemorySwap = int64(buildOptions["memory_swap"].(int))
buildImageOptions.CgroupParent = buildOptions["cgroup_parent"].(string)
buildImageOptions.NetworkMode = buildOptions["network_mode"].(string)
buildImageOptions.ShmSize = int64(buildOptions["shm_size"].(int))
buildImageOptions.Dockerfile = buildOptions["dockerfile"].(string)
buildImageOptions.Ulimits = readULimits(buildOptions["ulimit"].([]interface{}))
buildImageOptions.BuildArgs = mapToBuildArgs(buildOptions["build_args"].(map[string]interface{}))
buildImageOptions.AuthConfigs = readAuthConfigs(buildOptions["auth_config"].([]interface{}))
buildImageOptions.Labels = mapOfInterfacesToMapOfStrings(buildOptions["labels"].(map[string]interface{}))
buildImageOptions.Squash = buildOptions["squash"].(bool)
buildImageOptions.CacheFrom = interfaceArrayToStringArray(buildOptions["cache_from"].([]interface{}))
buildImageOptions.SecurityOpt = interfaceArrayToStringArray(buildOptions["security_opt"].([]interface{}))
buildImageOptions.ExtraHosts = interfaceArrayToStringArray(buildOptions["extra_hosts"].([]interface{}))
buildImageOptions.Target = buildOptions["target"].(string)
buildImageOptions.SessionID = buildOptions["session_id"].(string)
buildImageOptions.Platform = buildOptions["platform"].(string)
buildImageOptions.Version = build.BuilderVersion(buildOptions["version"].(string))
buildImageOptions.BuildID = buildOptions["build_id"].(string)
// outputs
return buildImageOptions
}
func pushDockerRegistryImage(ctx context.Context, client *client.Client, pushOpts internalPushImageOptions, username string, password string) error {
pushOptions := image.PushOptions{}
if username != "" {
auth := registry.AuthConfig{Username: username, Password: password}
authBytes, err := json.Marshal(auth)
if err != nil {
return fmt.Errorf("Error creating push options: %s", err)
}
authBase64 := base64.URLEncoding.EncodeToString(authBytes)
pushOptions.RegistryAuth = authBase64
}
log.Printf("[DEBUG] Pushing image %s with options %#v", pushOpts.FqName, pushOptions)
out, err := client.ImagePush(ctx, pushOpts.FqName, pushOptions)
if err != nil {
return err
}
defer out.Close() //nolint:errcheck
type ErrorMessage struct {
Error string
}
var errorMessage ErrorMessage
buffIOReader := bufio.NewReader(out)
for {
streamBytes, err := buffIOReader.ReadBytes('\n')
if err == io.EOF {
break
}
if err := json.Unmarshal(streamBytes, &errorMessage); err != nil {
return err
}
if errorMessage.Error != "" {
additionalMessage := createAdditionalErrorMessage(pushOpts.FqName)
return fmt.Errorf("Error pushing image. %s. Full error: %s", additionalMessage, errorMessage.Error)
}
}
log.Printf("[DEBUG] Pushed image: %s", pushOpts.FqName)
return nil
}
func createAdditionalErrorMessage(imageFqName string) string {
message := ""
if strings.HasPrefix(imageFqName, "public.ecr.aws/") {
message = "You are trying to push to a public ECR repository. One error cause might be that the image name does not have the correct format and registry alias: public.ecr.aws/<registry_alias>/<image>"
}
return message
}
func getAuthConfigForRegistry(
registryWithoutProtocol string,
providerConfig *ProviderConfig) (registry.AuthConfig, error) {
if authConfig, ok := providerConfig.AuthConfigs.Configs[registryWithoutProtocol]; ok {
return authConfig, nil
}
return registry.AuthConfig{}, fmt.Errorf("no auth config found for registry %s in auth configs: %#v", registryWithoutProtocol, providerConfig.AuthConfigs.Configs)
}
func buildHttpClientForRegistry(registryAddressWithProtocol string, insecureSkipVerify bool) *http.Client {
client := http.DefaultClient
if strings.HasPrefix(registryAddressWithProtocol, "https://") {
client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify}, Proxy: http.ProxyFromEnvironment}
} else {
client.Transport = &http.Transport{Proxy: http.ProxyFromEnvironment}
}
return client
}
func deleteDockerRegistryImage(pushOpts internalPushImageOptions, registryWithProtocol string, sha256Digest, username, password string, insecureSkipVerify, fallback bool) error {
client := buildHttpClientForRegistry(registryWithProtocol, insecureSkipVerify)
req, err := setupHTTPRequestForRegistry("DELETE", pushOpts.Registry, registryWithProtocol, pushOpts.Repository, sha256Digest, username, password, fallback)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("Error during registry request: %s", err)
}
switch resp.StatusCode {
// Basic auth was valid or not needed
case http.StatusOK, http.StatusAccepted, http.StatusNotFound:
return nil
// Either OAuth is required or the basic auth creds were invalid
case http.StatusUnauthorized:
auth, err := parseAuthHeader(resp.Header.Get("www-authenticate"))
if err != nil {
return fmt.Errorf("bad credentials: %s", resp.Status)
}
token, err := getAuthToken(auth, username, password, "repository:"+pushOpts.Repository+":*", client)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
oauthResp, err := client.Do(req)
if err != nil {
return err
}
switch oauthResp.StatusCode {
case http.StatusOK, http.StatusAccepted, http.StatusNotFound:
return nil
default:
return fmt.Errorf("got bad response from registry: %s", resp.Status)
}
// Some unexpected status was given, return an error
default:
return fmt.Errorf("got bad response from registry: %s", resp.Status)
}
}
func getImageDigestWithFallback(opts internalPushImageOptions, serverAddress string, username, password string, insecureSkipVerify bool) (string, error) {
digest, err := getImageDigest(opts.Registry, serverAddress, opts.Repository, opts.Tag, username, password, insecureSkipVerify, false)
if err != nil {
digest, err = getImageDigest(opts.Registry, serverAddress, opts.Repository, opts.Tag, username, password, insecureSkipVerify, true)
if err != nil {
return "", fmt.Errorf("unable to get digest: %s", err)
}
}
return digest, nil
}
func createPushImageOptions(image string) internalPushImageOptions {
pullOpts := parseImageOptions(image)
pushOpts := internalPushImageOptions{
Name: image,
Registry: pullOpts.Registry,
NormalizedRegistry: normalizeRegistryAddress(pullOpts.Registry),
Repository: pullOpts.Repository,
Tag: pullOpts.Tag,
FqName: fmt.Sprintf("%s/%s:%s", pullOpts.Registry, pullOpts.Repository, pullOpts.Tag),
}
return pushOpts
}