feat(deploy): add configurable image pull policy
- commit
- dcf17893c4c8a6bee6bfe83b9efd6b7deb6c0add
- author
- bnema · · 2026-01-16 20:50 UTC
- parent
- be93560
- signature
- unsigned
3 files · +89 -4
A docs/config/deploy.md +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | # Deploy Configuration | |
| 2 | + | ||
| 3 | + | Controls how Gordon pulls images during deployment. | |
| 4 | + | ||
| 5 | + | ## Example | |
| 6 | + | ||
| 7 | + | ```toml | |
| 8 | + | [deploy] | |
| 9 | + | pull_policy = "if-tag-changed" | |
| 10 | + | ``` | |
| 11 | + | ||
| 12 | + | ## Settings | |
| 13 | + | ||
| 14 | + | ### `deploy.pull_policy` | |
| 15 | + | ||
| 16 | + | How to decide when to pull an image: | |
| 17 | + | ||
| 18 | + | - `always`: always pull, even if the tag exists locally. | |
| 19 | + | - `if-not-present`: only pull when the tag is missing locally. | |
| 20 | + | - `if-tag-changed`: pull for tag references to check for updates; skip pulls for digest references (`@sha256:...`). | |
| 21 | + | ||
| 22 | + | Default: `if-tag-changed` | |
| 23 | + | ||
| 24 | + | ## Notes | |
| 25 | + | ||
| 26 | + | - Changes to `deploy.pull_policy` require a restart to take effect. | |
| @@ -0,0 +1,26 @@ | |||||
| 1 | + | # Deploy Configuration | |||
| 2 | + | ||||
| 3 | + | Controls how Gordon pulls images during deployment. | |||
| 4 | + | ||||
| 5 | + | ## Example | |||
| 6 | + | ||||
| 7 | + | ```toml | |||
| 8 | + | [deploy] | |||
| 9 | + | pull_policy = "if-tag-changed" | |||
| 10 | + | ``` | |||
| 11 | + | ||||
| 12 | + | ## Settings | |||
| 13 | + | ||||
| 14 | + | ### `deploy.pull_policy` | |||
| 15 | + | ||||
| 16 | + | How to decide when to pull an image: | |||
| 17 | + | ||||
| 18 | + | - `always`: always pull, even if the tag exists locally. | |||
| 19 | + | - `if-not-present`: only pull when the tag is missing locally. | |||
| 20 | + | - `if-tag-changed`: pull for tag references to check for updates; skip pulls for digest references (`@sha256:...`). | |||
| 21 | + | ||||
| 22 | + | Default: `if-tag-changed` | |||
| 23 | + | ||||
| 24 | + | ## Notes | |||
| 25 | + | ||||
| 26 | + | - Changes to `deploy.pull_policy` require a restart to take effect. | |||
M docs/config/index.md +10 -3
| @@ -7,7 +7,7 @@ Gordon uses a single TOML configuration file located at `~/.config/gordon/gordon | |||
| 7 | 7 | | Location | Purpose | | |
| 8 | 8 | |----------|---------| | |
| 9 | 9 | | `~/.config/gordon/gordon.toml` | Default configuration file | | |
| 10 | - | | Custom path via `--config` flag | `gordon start --config /path/to/config.toml` | | |
| 10 | + | | Custom path via `--config` flag | `gordon serve --config /path/to/config.toml` | | |
| 11 | 11 | ||
| 12 | 12 | ## Minimal Configuration | |
| 13 | 13 | ||
| @@ -48,6 +48,10 @@ type = "token" # "password" or "token" | |||
| 48 | 48 | token_secret = "gordon/registry/token_secret" | |
| 49 | 49 | token_expiry = "720h" # Duration or 0 for never | |
| 50 | 50 | ||
| 51 | + | # Deploy behavior | |
| 52 | + | [deploy] | |
| 53 | + | pull_policy = "if-tag-changed" # always, if-not-present, if-tag-changed | |
| 54 | + | ||
| 51 | 55 | # Logging | |
| 52 | 56 | [logging] | |
| 53 | 57 | level = "info" # trace, debug, info, warn, error | |
| @@ -109,6 +113,7 @@ enabled = false # Auto-create routes from image names | |||
| 109 | 113 | | `[server]` | Core server settings | [Server](./server.md) | | |
| 110 | 114 | | `[secrets]` | Secrets backend configuration | [Secrets](./secrets.md) | | |
| 111 | 115 | | `[registry_auth]` | Registry authentication | [Registry Auth](./registry-auth.md) | | |
| 116 | + | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | |
| 112 | 117 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | |
| 113 | 118 | | `[env]` | Environment variable settings | [Environment](./env.md) | | |
| 114 | 119 | | `[volumes]` | Volume management | [Volumes](./volumes.md) | | |
| @@ -130,6 +135,7 @@ enabled = false # Auto-create routes from image names | |||
| 130 | 135 | | `registry_auth.enabled` | `false` | | |
| 131 | 136 | | `registry_auth.type` | `"password"` | | |
| 132 | 137 | | `registry_auth.token_expiry` | `"720h"` | | |
| 138 | + | | `deploy.pull_policy` | `"if-tag-changed"` | | |
| 133 | 139 | | `logging.level` | `"info"` | | |
| 134 | 140 | | `logging.format` | `"console"` | | |
| 135 | 141 | | `logging.file.enabled` | `false` | | |
| @@ -158,14 +164,15 @@ The following settings require a restart to take effect: | |||
| 158 | 164 | - `server.data_dir` | |
| 159 | 165 | - `secrets.backend` | |
| 160 | 166 | - `registry_auth` settings | |
| 167 | + | - `deploy.pull_policy` | |
| 161 | 168 | ||
| 162 | 169 | ## Environment Variable Override | |
| 163 | 170 | ||
| 164 | 171 | Configuration values can be overridden with environment variables: | |
| 165 | 172 | ||
| 166 | 173 | ```bash | |
| 167 | - | GORDON_SERVER_PORT=8080 gordon start | |
| 168 | - | GORDON_LOGGING_LEVEL=debug gordon start | |
| 174 | + | GORDON_SERVER_PORT=8080 gordon serve | |
| 175 | + | GORDON_LOGGING_LEVEL=debug gordon serve | |
| 169 | 176 | ``` | |
| 170 | 177 | ||
| 171 | 178 | Pattern: `GORDON_SECTION_KEY` (uppercase, underscores instead of dots) | |
| @@ -7,7 +7,7 @@ Gordon uses a single TOML configuration file located at `~/.config/gordon/gordon | |||||
| 7 | | Location | Purpose | | 7 | | Location | Purpose | | ||
| 8 | |----------|---------| | 8 | |----------|---------| | ||
| 9 | | `~/.config/gordon/gordon.toml` | Default configuration file | | 9 | | `~/.config/gordon/gordon.toml` | Default configuration file | | ||
| 10 | - | | Custom path via `--config` flag | `gordon start --config /path/to/config.toml` | | 10 | + | | Custom path via `--config` flag | `gordon serve --config /path/to/config.toml` | |
| 11 | 11 | ||||
| 12 | ## Minimal Configuration | 12 | ## Minimal Configuration | ||
| 13 | 13 | ||||
| @@ -48,6 +48,10 @@ type = "token" # "password" or "token" | |||||
| 48 | token_secret = "gordon/registry/token_secret" | 48 | token_secret = "gordon/registry/token_secret" | ||
| 49 | token_expiry = "720h" # Duration or 0 for never | 49 | token_expiry = "720h" # Duration or 0 for never | ||
| 50 | 50 | ||||
| 51 | + | # Deploy behavior | |||
| 52 | + | [deploy] | |||
| 53 | + | pull_policy = "if-tag-changed" # always, if-not-present, if-tag-changed | |||
| 54 | + | ||||
| 51 | # Logging | 55 | # Logging | ||
| 52 | [logging] | 56 | [logging] | ||
| 53 | level = "info" # trace, debug, info, warn, error | 57 | level = "info" # trace, debug, info, warn, error | ||
| @@ -109,6 +113,7 @@ enabled = false # Auto-create routes from image names | |||||
| 109 | | `[server]` | Core server settings | [Server](./server.md) | | 113 | | `[server]` | Core server settings | [Server](./server.md) | | ||
| 110 | | `[secrets]` | Secrets backend configuration | [Secrets](./secrets.md) | | 114 | | `[secrets]` | Secrets backend configuration | [Secrets](./secrets.md) | | ||
| 111 | | `[registry_auth]` | Registry authentication | [Registry Auth](./registry-auth.md) | | 115 | | `[registry_auth]` | Registry authentication | [Registry Auth](./registry-auth.md) | | ||
| 116 | + | | `[deploy]` | Deployment behavior | [Deploy](./deploy.md) | | |||
| 112 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | 117 | | `[logging]` | Logging configuration | [Logging](./logging.md) | | ||
| 113 | | `[env]` | Environment variable settings | [Environment](./env.md) | | 118 | | `[env]` | Environment variable settings | [Environment](./env.md) | | ||
| 114 | | `[volumes]` | Volume management | [Volumes](./volumes.md) | | 119 | | `[volumes]` | Volume management | [Volumes](./volumes.md) | | ||
| @@ -130,6 +135,7 @@ enabled = false # Auto-create routes from image names | |||||
| 130 | | `registry_auth.enabled` | `false` | | 135 | | `registry_auth.enabled` | `false` | | ||
| 131 | | `registry_auth.type` | `"password"` | | 136 | | `registry_auth.type` | `"password"` | | ||
| 132 | | `registry_auth.token_expiry` | `"720h"` | | 137 | | `registry_auth.token_expiry` | `"720h"` | | ||
| 138 | + | | `deploy.pull_policy` | `"if-tag-changed"` | | |||
| 133 | | `logging.level` | `"info"` | | 139 | | `logging.level` | `"info"` | | ||
| 134 | | `logging.format` | `"console"` | | 140 | | `logging.format` | `"console"` | | ||
| 135 | | `logging.file.enabled` | `false` | | 141 | | `logging.file.enabled` | `false` | | ||
| @@ -158,14 +164,15 @@ The following settings require a restart to take effect: | |||||
| 158 | - `server.data_dir` | 164 | - `server.data_dir` | ||
| 159 | - `secrets.backend` | 165 | - `secrets.backend` | ||
| 160 | - `registry_auth` settings | 166 | - `registry_auth` settings | ||
| 167 | + | - `deploy.pull_policy` | |||
| 161 | 168 | ||||
| 162 | ## Environment Variable Override | 169 | ## Environment Variable Override | ||
| 163 | 170 | ||||
| 164 | Configuration values can be overridden with environment variables: | 171 | Configuration values can be overridden with environment variables: | ||
| 165 | 172 | ||||
| 166 | ```bash | 173 | ```bash | ||
| 167 | - | GORDON_SERVER_PORT=8080 gordon start | 174 | + | GORDON_SERVER_PORT=8080 gordon serve |
| 168 | - | GORDON_LOGGING_LEVEL=debug gordon start | 175 | + | GORDON_LOGGING_LEVEL=debug gordon serve |
| 169 | ``` | 176 | ``` | ||
| 170 | 177 | ||||
| 171 | Pattern: `GORDON_SECTION_KEY` (uppercase, underscores instead of dots) | 178 | Pattern: `GORDON_SECTION_KEY` (uppercase, underscores instead of dots) | ||
M internal/usecase/container/service.go +53 -1
| @@ -25,6 +25,7 @@ type Config struct { | |||
| 25 | 25 | RegistryPassword string | |
| 26 | 26 | InternalRegistryUsername string | |
| 27 | 27 | InternalRegistryPassword string | |
| 28 | + | PullPolicy string | |
| 28 | 29 | VolumeAutoCreate bool | |
| 29 | 30 | VolumePrefix string | |
| 30 | 31 | VolumePreserve bool | |
| @@ -36,6 +37,12 @@ type Config struct { | |||
| 36 | 37 | ReadinessDelay time.Duration // Delay after container starts before considering it ready | |
| 37 | 38 | } | |
| 38 | 39 | ||
| 40 | + | const ( | |
| 41 | + | PullPolicyAlways = "always" | |
| 42 | + | PullPolicyIfNotPresent = "if-not-present" | |
| 43 | + | PullPolicyIfTagChanged = "if-tag-changed" | |
| 44 | + | ) | |
| 45 | + | ||
| 39 | 46 | // Service implements the ContainerService interface. | |
| 40 | 47 | type Service struct { | |
| 41 | 48 | runtime out.ContainerRuntime | |
| @@ -513,6 +520,23 @@ func (s *Service) buildImageRef(image string) string { | |||
| 513 | 520 | return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image) | |
| 514 | 521 | } | |
| 515 | 522 | ||
| 523 | + | func normalizePullPolicy(policy string) string { | |
| 524 | + | switch strings.ToLower(strings.TrimSpace(policy)) { | |
| 525 | + | case PullPolicyAlways: | |
| 526 | + | return PullPolicyAlways | |
| 527 | + | case PullPolicyIfTagChanged: | |
| 528 | + | return PullPolicyIfTagChanged | |
| 529 | + | case PullPolicyIfNotPresent: | |
| 530 | + | return PullPolicyIfNotPresent | |
| 531 | + | default: | |
| 532 | + | return PullPolicyIfNotPresent | |
| 533 | + | } | |
| 534 | + | } | |
| 535 | + | ||
| 536 | + | func isDigestRef(imageRef string) bool { | |
| 537 | + | return strings.Contains(imageRef, "@sha256:") | |
| 538 | + | } | |
| 539 | + | ||
| 516 | 540 | func (s *Service) pullRefForDeploy(ctx context.Context, imageRef string) (string, bool) { | |
| 517 | 541 | if !domain.IsInternalDeploy(ctx) { | |
| 518 | 542 | return imageRef, false | |
| @@ -521,7 +545,7 @@ func (s *Service) pullRefForDeploy(ctx context.Context, imageRef string) (string | |||
| 521 | 545 | } | |
| 522 | 546 | ||
| 523 | 547 | // ensureImage ensures the image is available locally, pulling if needed. | |
| 524 | - | // Returns the canonical image reference to use for container operations. | |
| 548 | + | // Returns the image reference to use for container operations. | |
| 525 | 549 | func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, error) { | |
| 526 | 550 | ctx = zerowrap.CtxWithField(ctx, "image", imageRef) | |
| 527 | 551 | log := zerowrap.FromCtx(ctx) | |
| @@ -550,6 +574,15 @@ func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, err | |||
| 550 | 574 | return "", err | |
| 551 | 575 | } | |
| 552 | 576 | ||
| 577 | + | // For digest references, we can't create a tag (Docker doesn't allow it). | |
| 578 | + | // In this case, use the pullRef directly since the image is available under that reference. | |
| 579 | + | if strings.Contains(imageRef, "@sha256:") { | |
| 580 | + | log.Info(). | |
| 581 | + | Str("pull_ref", pullRef). | |
| 582 | + | Msg("digest reference: using pull reference for container operations") | |
| 583 | + | return pullRef, nil | |
| 584 | + | } | |
| 585 | + | ||
| 553 | 586 | if err := s.tagImageIfNeeded(ctx, pullRef, imageRef); err != nil { | |
| 554 | 587 | return "", err | |
| 555 | 588 | } | |
| @@ -576,6 +609,18 @@ func (s *Service) ensureLocalImage(ctx context.Context, imageRef, pullRef string | |||
| 576 | 609 | return false, nil | |
| 577 | 610 | } | |
| 578 | 611 | ||
| 612 | + | pullPolicy := normalizePullPolicy(s.config.PullPolicy) | |
| 613 | + | switch pullPolicy { | |
| 614 | + | case PullPolicyAlways: | |
| 615 | + | log.Info().Str("pull_policy", pullPolicy).Msg("pull policy forces image pull") | |
| 616 | + | return false, nil | |
| 617 | + | case PullPolicyIfTagChanged: | |
| 618 | + | if !isDigestRef(imageRef) { | |
| 619 | + | log.Info().Str("pull_policy", pullPolicy).Msg("tag reference detected, pulling to check for updates") | |
| 620 | + | return false, nil | |
| 621 | + | } | |
| 622 | + | } | |
| 623 | + | ||
| 579 | 624 | localImages, err := s.runtime.ListImages(ctx) | |
| 580 | 625 | if err != nil { | |
| 581 | 626 | log.WrapErr(err, "failed to list local images, will attempt pull") | |
| @@ -641,6 +686,13 @@ func (s *Service) tagImageIfNeeded(ctx context.Context, sourceRef, targetRef str | |||
| 641 | 686 | return nil | |
| 642 | 687 | } | |
| 643 | 688 | ||
| 689 | + | // Cannot create a tag with a digest reference - Docker/Podman doesn't allow it. | |
| 690 | + | // When using digest references (image@sha256:...), skip tagging as the image | |
| 691 | + | // is already available by its digest. | |
| 692 | + | if strings.Contains(targetRef, "@sha256:") { | |
| 693 | + | return nil | |
| 694 | + | } | |
| 695 | + | ||
| 644 | 696 | log := zerowrap.FromCtx(ctx) | |
| 645 | 697 | if err := s.runtime.TagImage(ctx, sourceRef, targetRef); err != nil { | |
| 646 | 698 | return log.WrapErr(err, "failed to tag image from pull reference") | |
| @@ -25,6 +25,7 @@ type Config struct { | |||||
| 25 | RegistryPassword string | 25 | RegistryPassword string | ||
| 26 | InternalRegistryUsername string | 26 | InternalRegistryUsername string | ||
| 27 | InternalRegistryPassword string | 27 | InternalRegistryPassword string | ||
| 28 | + | PullPolicy string | |||
| 28 | VolumeAutoCreate bool | 29 | VolumeAutoCreate bool | ||
| 29 | VolumePrefix string | 30 | VolumePrefix string | ||
| 30 | VolumePreserve bool | 31 | VolumePreserve bool | ||
| @@ -36,6 +37,12 @@ type Config struct { | |||||
| 36 | ReadinessDelay time.Duration // Delay after container starts before considering it ready | 37 | ReadinessDelay time.Duration // Delay after container starts before considering it ready | ||
| 37 | } | 38 | } | ||
| 38 | 39 | ||||
| 40 | + | const ( | |||
| 41 | + | PullPolicyAlways = "always" | |||
| 42 | + | PullPolicyIfNotPresent = "if-not-present" | |||
| 43 | + | PullPolicyIfTagChanged = "if-tag-changed" | |||
| 44 | + | ) | |||
| 45 | + | ||||
| 39 | // Service implements the ContainerService interface. | 46 | // Service implements the ContainerService interface. | ||
| 40 | type Service struct { | 47 | type Service struct { | ||
| 41 | runtime out.ContainerRuntime | 48 | runtime out.ContainerRuntime | ||
| @@ -513,6 +520,23 @@ func (s *Service) buildImageRef(image string) string { | |||||
| 513 | return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image) | 520 | return fmt.Sprintf("%s/%s", s.config.RegistryDomain, image) | ||
| 514 | } | 521 | } | ||
| 515 | 522 | ||||
| 523 | + | func normalizePullPolicy(policy string) string { | |||
| 524 | + | switch strings.ToLower(strings.TrimSpace(policy)) { | |||
| 525 | + | case PullPolicyAlways: | |||
| 526 | + | return PullPolicyAlways | |||
| 527 | + | case PullPolicyIfTagChanged: | |||
| 528 | + | return PullPolicyIfTagChanged | |||
| 529 | + | case PullPolicyIfNotPresent: | |||
| 530 | + | return PullPolicyIfNotPresent | |||
| 531 | + | default: | |||
| 532 | + | return PullPolicyIfNotPresent | |||
| 533 | + | } | |||
| 534 | + | } | |||
| 535 | + | ||||
| 536 | + | func isDigestRef(imageRef string) bool { | |||
| 537 | + | return strings.Contains(imageRef, "@sha256:") | |||
| 538 | + | } | |||
| 539 | + | ||||
| 516 | func (s *Service) pullRefForDeploy(ctx context.Context, imageRef string) (string, bool) { | 540 | func (s *Service) pullRefForDeploy(ctx context.Context, imageRef string) (string, bool) { | ||
| 517 | if !domain.IsInternalDeploy(ctx) { | 541 | if !domain.IsInternalDeploy(ctx) { | ||
| 518 | return imageRef, false | 542 | return imageRef, false | ||
| @@ -521,7 +545,7 @@ func (s *Service) pullRefForDeploy(ctx context.Context, imageRef string) (string | |||||
| 521 | } | 545 | } | ||
| 522 | 546 | ||||
| 523 | // ensureImage ensures the image is available locally, pulling if needed. | 547 | // ensureImage ensures the image is available locally, pulling if needed. | ||
| 524 | - | // Returns the canonical image reference to use for container operations. | 548 | + | // Returns the image reference to use for container operations. |
| 525 | func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, error) { | 549 | func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, error) { | ||
| 526 | ctx = zerowrap.CtxWithField(ctx, "image", imageRef) | 550 | ctx = zerowrap.CtxWithField(ctx, "image", imageRef) | ||
| 527 | log := zerowrap.FromCtx(ctx) | 551 | log := zerowrap.FromCtx(ctx) | ||
| @@ -550,6 +574,15 @@ func (s *Service) ensureImage(ctx context.Context, imageRef string) (string, err | |||||
| 550 | return "", err | 574 | return "", err | ||
| 551 | } | 575 | } | ||
| 552 | 576 | ||||
| 577 | + | // For digest references, we can't create a tag (Docker doesn't allow it). | |||
| 578 | + | // In this case, use the pullRef directly since the image is available under that reference. | |||
| 579 | + | if strings.Contains(imageRef, "@sha256:") { | |||
| 580 | + | log.Info(). | |||
| 581 | + | Str("pull_ref", pullRef). | |||
| 582 | + | Msg("digest reference: using pull reference for container operations") | |||
| 583 | + | return pullRef, nil | |||
| 584 | + | } | |||
| 585 | + | ||||
| 553 | if err := s.tagImageIfNeeded(ctx, pullRef, imageRef); err != nil { | 586 | if err := s.tagImageIfNeeded(ctx, pullRef, imageRef); err != nil { | ||
| 554 | return "", err | 587 | return "", err | ||
| 555 | } | 588 | } | ||
| @@ -576,6 +609,18 @@ func (s *Service) ensureLocalImage(ctx context.Context, imageRef, pullRef string | |||||
| 576 | return false, nil | 609 | return false, nil | ||
| 577 | } | 610 | } | ||
| 578 | 611 | ||||
| 612 | + | pullPolicy := normalizePullPolicy(s.config.PullPolicy) | |||
| 613 | + | switch pullPolicy { | |||
| 614 | + | case PullPolicyAlways: | |||
| 615 | + | log.Info().Str("pull_policy", pullPolicy).Msg("pull policy forces image pull") | |||
| 616 | + | return false, nil | |||
| 617 | + | case PullPolicyIfTagChanged: | |||
| 618 | + | if !isDigestRef(imageRef) { | |||
| 619 | + | log.Info().Str("pull_policy", pullPolicy).Msg("tag reference detected, pulling to check for updates") | |||
| 620 | + | return false, nil | |||
| 621 | + | } | |||
| 622 | + | } | |||
| 623 | + | ||||
| 579 | localImages, err := s.runtime.ListImages(ctx) | 624 | localImages, err := s.runtime.ListImages(ctx) | ||
| 580 | if err != nil { | 625 | if err != nil { | ||
| 581 | log.WrapErr(err, "failed to list local images, will attempt pull") | 626 | log.WrapErr(err, "failed to list local images, will attempt pull") | ||
| @@ -641,6 +686,13 @@ func (s *Service) tagImageIfNeeded(ctx context.Context, sourceRef, targetRef str | |||||
| 641 | return nil | 686 | return nil | ||
| 642 | } | 687 | } | ||
| 643 | 688 | ||||
| 689 | + | // Cannot create a tag with a digest reference - Docker/Podman doesn't allow it. | |||
| 690 | + | // When using digest references (image@sha256:...), skip tagging as the image | |||
| 691 | + | // is already available by its digest. | |||
| 692 | + | if strings.Contains(targetRef, "@sha256:") { | |||
| 693 | + | return nil | |||
| 694 | + | } | |||
| 695 | + | ||||
| 644 | log := zerowrap.FromCtx(ctx) | 696 | log := zerowrap.FromCtx(ctx) | ||
| 645 | if err := s.runtime.TagImage(ctx, sourceRef, targetRef); err != nil { | 697 | if err := s.runtime.TagImage(ctx, sourceRef, targetRef); err != nil { | ||
| 646 | return log.WrapErr(err, "failed to tag image from pull reference") | 698 | return log.WrapErr(err, "failed to tag image from pull reference") | ||