@@ -19,6 +19,7 @@ package docker
1919import (
2020 "context"
2121 "os"
22+ "strings"
2223 "testing"
2324
2425 "github.com/docker/docker/api/types/container"
@@ -29,7 +30,10 @@ import (
2930 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/debug/types"
3031 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/deploy/label"
3132 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker/debugger"
33+ "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/docker/tracker"
3234 "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"
3337 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/latest"
3438 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/schema/util"
3539 "github.com/GoogleContainerTools/skaffold/v2/testutil"
@@ -542,3 +546,142 @@ func TestDeployWithComposeFileOperations(t *testing.T) {
542546func (m mockConfig ) MinikubeProfile () string { return "" }
543547func (m mockConfig ) Mode () config.RunMode { return "" }
544548func (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