Skip to content

Commit a655ef3

Browse files
committed
fix image name matching logic
1 parent a530725 commit a655ef3

File tree

2 files changed

+93
-149
lines changed

2 files changed

+93
-149
lines changed

pkg/skaffold/deploy/docker/deploy.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,28 @@ func (d *Deployer) getComposeFilePath() string {
636636
return "docker-compose.yml"
637637
}
638638

639+
// imageNameMatches checks if a compose image name matches an artifact's built image.
640+
// It handles cases like:
641+
// - "frontend" matching "frontend:tag" or "gcr.io/project/frontend:tag"
642+
// - "gcr.io/project/frontend" matching "gcr.io/project/frontend:tag"
643+
// But rejects false positives like:
644+
// - "app" should NOT match "my-app"
645+
func imageNameMatches(composeImage, artifactTag string) bool {
646+
// Remove tags from both images
647+
composeBase := strings.Split(composeImage, ":")[0]
648+
artifactBase := strings.Split(artifactTag, ":")[0]
649+
650+
// Exact match (most common case)
651+
if composeBase == artifactBase {
652+
return true
653+
}
654+
655+
// Check if artifact image ends with compose image (handles registry prefixes)
656+
// e.g., "frontend" matches "gcr.io/project/frontend"
657+
// But "app" does NOT match "my-app" (no "/" separator)
658+
return strings.HasSuffix(artifactBase, "/"+composeBase)
659+
}
660+
639661
// replaceComposeImages replaces image names in compose config with skaffold-built images
640662
func (d *Deployer) replaceComposeImages(composeConfig map[string]interface{}, artifact graph.Artifact) error {
641663
services, ok := composeConfig["services"].(map[string]interface{})
@@ -652,8 +674,8 @@ func (d *Deployer) replaceComposeImages(composeConfig map[string]interface{}, ar
652674

653675
// Check if service has an image field
654676
if imageName, ok := service["image"].(string); ok {
655-
// Check if this image matches the artifact's image name
656-
if imageName == artifact.ImageName || strings.Contains(artifact.ImageName, imageName) {
677+
// Check if this image matches the artifact's built image
678+
if imageNameMatches(imageName, artifact.Tag) {
657679
olog.Entry(context.Background()).Debugf("Replacing image for service %s: %s -> %s", serviceName, imageName, artifact.Tag)
658680
service["image"] = artifact.Tag
659681
}

pkg/skaffold/deploy/docker/deploy_test.go

Lines changed: 69 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package docker
1919
import (
2020
"context"
2121
"os"
22-
"strings"
2322
"testing"
2423

2524
"github.com/docker/docker/api/types/container"
@@ -30,10 +29,7 @@ import (
3029
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/debug/types"
3130
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/label"
3231
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker/debugger"
33-
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker/tracker"
3432
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/graph"
35-
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/kubernetes/manifest"
36-
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/log"
3733
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
3834
"github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/util"
3935
"github.com/GoogleContainerTools/skaffold/v2/testutil"
@@ -284,6 +280,9 @@ func (m mockConfig) ContainerDebugging() bool { return true }
284280
func (m mockConfig) GetInsecureRegistries() map[string]bool { return nil }
285281
func (m mockConfig) GetKubeContext() string { return "" }
286282
func (m mockConfig) GlobalConfig() string { return "" }
283+
func (m mockConfig) MinikubeProfile() string { return "" }
284+
func (m mockConfig) Mode() config.RunMode { return "" }
285+
func (m mockConfig) Prune() bool { return false }
287286

288287
// Tests for Docker Compose deployer
289288

@@ -407,6 +406,72 @@ func TestReplaceComposeImages(t *testing.T) {
407406
},
408407
shouldErr: false,
409408
},
409+
{
410+
name: "reject false positive: 'app' should not match 'my-app'",
411+
composeConfig: map[string]interface{}{
412+
"services": map[string]interface{}{
413+
"myapp": map[string]interface{}{
414+
"image": "app",
415+
},
416+
},
417+
},
418+
artifact: graph.Artifact{
419+
ImageName: "my-app",
420+
Tag: "my-app:v1.0.0",
421+
},
422+
expectedConfig: map[string]interface{}{
423+
"services": map[string]interface{}{
424+
"myapp": map[string]interface{}{
425+
"image": "app", // Should NOT be replaced
426+
},
427+
},
428+
},
429+
shouldErr: false,
430+
},
431+
{
432+
name: "match with registry prefix",
433+
composeConfig: map[string]interface{}{
434+
"services": map[string]interface{}{
435+
"frontend": map[string]interface{}{
436+
"image": "frontend",
437+
},
438+
},
439+
},
440+
artifact: graph.Artifact{
441+
ImageName: "frontend",
442+
Tag: "gcr.io/project/frontend:sha256-abc",
443+
},
444+
expectedConfig: map[string]interface{}{
445+
"services": map[string]interface{}{
446+
"frontend": map[string]interface{}{
447+
"image": "gcr.io/project/frontend:sha256-abc",
448+
},
449+
},
450+
},
451+
shouldErr: false,
452+
},
453+
{
454+
name: "match with tag in compose file",
455+
composeConfig: map[string]interface{}{
456+
"services": map[string]interface{}{
457+
"backend": map[string]interface{}{
458+
"image": "backend:latest",
459+
},
460+
},
461+
},
462+
artifact: graph.Artifact{
463+
ImageName: "backend",
464+
Tag: "backend:v2.0.0",
465+
},
466+
expectedConfig: map[string]interface{}{
467+
"services": map[string]interface{}{
468+
"backend": map[string]interface{}{
469+
"image": "backend:v2.0.0",
470+
},
471+
},
472+
},
473+
shouldErr: false,
474+
},
410475
}
411476

412477
for _, test := range tests {
@@ -542,146 +607,3 @@ func TestDeployWithComposeFileOperations(t *testing.T) {
542607
})
543608
}
544609
}
545-
546-
func (m mockConfig) MinikubeProfile() string { return "" }
547-
func (m mockConfig) Mode() config.RunMode { return "" }
548-
func (m mockConfig) Prune() bool { return false }
549-
550-
// createMockDockerCommand creates a fake 'docker' command that records invocations
551-
// This allows us to test command execution without modifying production code
552-
func createMockDockerCommand(t *testing.T, tmpDir string) (binPath string, counterFile string) {
553-
t.Helper()
554-
555-
// Create a bin directory for our fake docker command
556-
binDir := tmpDir + "/bin"
557-
if err := os.MkdirAll(binDir, 0755); err != nil {
558-
t.Fatalf("Failed to create bin directory: %v", err)
559-
}
560-
561-
// Counter file to track invocations
562-
counterFile = tmpDir + "/docker-call-count.txt"
563-
564-
// Create a fake docker script that records calls
565-
dockerScript := binDir + "/docker"
566-
scriptContent := `#!/bin/bash
567-
# Mock docker command that tracks invocations
568-
echo "1" >> "` + counterFile + `"
569-
570-
# Log the arguments for debugging
571-
echo "$@" >> "` + tmpDir + `/docker-args.log"
572-
573-
# Exit successfully
574-
exit 0
575-
`
576-
if err := os.WriteFile(dockerScript, []byte(scriptContent), 0755); err != nil {
577-
t.Fatalf("Failed to create mock docker script: %v", err)
578-
}
579-
580-
return binDir, counterFile
581-
}
582-
583-
// TestDeployWithCompose_MultipleArtifacts_CallsComposeUpOnce verifies that docker compose up
584-
// is called only once when deploying multiple artifacts, with all image replacements done together.
585-
// This test uses a mock docker command to track invocations without modifying production code.
586-
func TestDeployWithCompose_MultipleArtifacts_CallsComposeUpOnce(t *testing.T) {
587-
testutil.Run(t, "multiple artifacts trigger compose up only once", func(tt *testutil.T) {
588-
// Create temporary directory for test files
589-
tmpDir := t.TempDir()
590-
591-
// Create mock docker command
592-
binDir, counterFile := createMockDockerCommand(t, tmpDir)
593-
594-
// Modify PATH to use our mock docker command
595-
originalPath := os.Getenv("PATH")
596-
os.Setenv("PATH", binDir+":"+originalPath)
597-
defer os.Setenv("PATH", originalPath)
598-
599-
// Create a temporary compose file for testing
600-
composeFile := tmpDir + "/docker-compose.yml"
601-
composeContent := `version: '3'
602-
services:
603-
frontend:
604-
image: frontend
605-
backend:
606-
image: backend
607-
`
608-
if err := os.WriteFile(composeFile, []byte(composeContent), 0644); err != nil {
609-
t.Fatalf("Failed to write compose file: %v", err)
610-
}
611-
612-
// Set environment variable to use our test compose file
613-
originalEnv := os.Getenv("SKAFFOLD_COMPOSE_FILE")
614-
os.Setenv("SKAFFOLD_COMPOSE_FILE", composeFile)
615-
defer func() {
616-
if originalEnv != "" {
617-
os.Setenv("SKAFFOLD_COMPOSE_FILE", originalEnv)
618-
} else {
619-
os.Unsetenv("SKAFFOLD_COMPOSE_FILE")
620-
}
621-
}()
622-
623-
// Create a minimal deployer
624-
d := &Deployer{
625-
cfg: &latest.DockerDeploy{
626-
UseCompose: true,
627-
Images: []string{"frontend", "backend"},
628-
},
629-
labeller: &label.DefaultLabeller{},
630-
tracker: tracker.NewContainerTracker(),
631-
logger: &log.NoopLogger{},
632-
}
633-
634-
// Skip network creation
635-
d.once.Do(func() {})
636-
637-
// Create multiple artifacts
638-
builds := []graph.Artifact{
639-
{
640-
ImageName: "frontend",
641-
Tag: "frontend:v1.0.0",
642-
},
643-
{
644-
ImageName: "backend",
645-
Tag: "backend:v1.0.0",
646-
},
647-
}
648-
649-
// Call Deploy
650-
err := d.Deploy(context.Background(), os.Stdout, builds, manifest.ManifestListByConfig{})
651-
if err != nil {
652-
t.Fatalf("Deploy failed: %v", err)
653-
}
654-
655-
// Read the counter file to check how many times docker was called
656-
counterData, err := os.ReadFile(counterFile)
657-
if err != nil {
658-
t.Fatalf("Failed to read counter file: %v", err)
659-
}
660-
661-
// Count lines (each invocation adds one line)
662-
lines := 0
663-
for _, b := range counterData {
664-
if b == '\n' {
665-
lines++
666-
}
667-
}
668-
669-
// FIXED: docker compose up should be called only once with all artifacts
670-
if lines != 1 {
671-
t.Errorf("Expected docker compose up to be called 1 time, but was called %d times", lines)
672-
}
673-
674-
// Log arguments for verification
675-
argsData, _ := os.ReadFile(tmpDir + "/docker-args.log")
676-
t.Logf("SUCCESS: docker compose up was called %d time for %d artifacts", lines, len(builds))
677-
t.Logf("Docker command arguments:\n%s", string(argsData))
678-
679-
// Verify it was a compose up command
680-
if lines == 1 {
681-
argsStr := string(argsData)
682-
if !strings.Contains(argsStr, "compose") || !strings.Contains(argsStr, "up") {
683-
t.Errorf("Expected 'docker compose up' command, got: %s", argsStr)
684-
}
685-
}
686-
})
687-
}

0 commit comments

Comments
 (0)