Skip to content

Commit a530725

Browse files
committed
fix artifacts loop issue
1 parent cdd809e commit a530725

File tree

2 files changed

+161
-9
lines changed

2 files changed

+161
-9
lines changed

pkg/skaffold/deploy/docker/deploy.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ func (d *Deployer) Deploy(ctx context.Context, out io.Writer, builds []graph.Art
143143
return fmt.Errorf("creating skaffold network %s: %w", d.network, err)
144144
}
145145

146+
// If using docker compose, deploy all artifacts at once
147+
if d.cfg.UseCompose {
148+
return d.deployAllWithCompose(ctx, out, builds)
149+
}
150+
146151
// TODO(nkubala)[07/20/21]: parallelize with sync.Errgroup
147152
for _, b := range builds {
148153
if err := d.deploy(ctx, out, b); err != nil {
@@ -172,9 +177,6 @@ func (d *Deployer) deploy(ctx context.Context, out io.Writer, artifact graph.Art
172177
}
173178
d.portManager.RelinquishPorts(container.Name)
174179
}
175-
if d.cfg.UseCompose {
176-
return d.deployWithCompose(ctx, out, artifact)
177-
}
178180

179181
containerCfg, err := d.containerConfigFromImage(ctx, artifact.Tag)
180182
if err != nil {
@@ -554,8 +556,9 @@ func (d *Deployer) RegisterLocalImages([]graph.Artifact) {
554556
// all images are local, so this is a noop
555557
}
556558

557-
// deployWithCompose deploys using docker compose
558-
func (d *Deployer) deployWithCompose(ctx context.Context, out io.Writer, artifact graph.Artifact) error {
559+
// deployAllWithCompose deploys all artifacts at once using docker compose.
560+
// This ensures that docker compose up is called only once with all image replacements.
561+
func (d *Deployer) deployAllWithCompose(ctx context.Context, out io.Writer, artifacts []graph.Artifact) error {
559562
// Find compose file path (default: docker-compose.yml in current directory)
560563
composeFile := d.getComposeFilePath()
561564

@@ -578,9 +581,11 @@ func (d *Deployer) deployWithCompose(ctx context.Context, out io.Writer, artifac
578581
return fmt.Errorf("failed to parse compose file: %w", err)
579582
}
580583

581-
// Replace image names with skaffold-built images
582-
if err := d.replaceComposeImages(composeConfig, artifact); err != nil {
583-
return fmt.Errorf("failed to replace images in compose file: %w", err)
584+
// Replace image names with skaffold-built images for ALL artifacts
585+
for _, artifact := range artifacts {
586+
if err := d.replaceComposeImages(composeConfig, artifact); err != nil {
587+
return fmt.Errorf("failed to replace images in compose file: %w", err)
588+
}
584589
}
585590

586591
// Write modified compose file to temp location
@@ -601,7 +606,7 @@ func (d *Deployer) deployWithCompose(ctx context.Context, out io.Writer, artifac
601606
}
602607
tmpComposeFile.Close()
603608

604-
// Run docker compose up
609+
// Run docker compose up (only once for all artifacts)
605610
args := []string{"compose", "-f", tmpComposeFile.Name(), "-p", fmt.Sprintf("skaffold-%s", d.labeller.GetRunID()), "up", "-d"}
606611
cmd := exec.CommandContext(ctx, "docker", args...)
607612
cmd.Stdout = out
@@ -614,6 +619,10 @@ func (d *Deployer) deployWithCompose(ctx context.Context, out io.Writer, artifac
614619
}
615620

616621
olog.Entry(ctx).Infof("Successfully deployed with docker compose")
622+
623+
// Track all build artifacts
624+
d.TrackBuildArtifacts(artifacts, nil)
625+
617626
return nil
618627
}
619628

pkg/skaffold/deploy/docker/deploy_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package docker
1919
import (
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) {
542546
func (m mockConfig) MinikubeProfile() string { return "" }
543547
func (m mockConfig) Mode() config.RunMode { return "" }
544548
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)