@@ -20,6 +20,8 @@ import (
2020 "context"
2121 "fmt"
2222 "io"
23+ "os"
24+ "os/exec"
2325 "regexp"
2426 "strings"
2527 "sync"
@@ -34,6 +36,7 @@ import (
3436 "github.com/docker/go-connections/nat"
3537 v1 "github.com/google/go-containerregistry/pkg/v1"
3638 "github.com/pkg/errors"
39+ "gopkg.in/yaml.v3"
3740
3841 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/access"
3942 "github.com/GoogleContainerTools/skaffold/v2/pkg/skaffold/config"
@@ -170,8 +173,7 @@ func (d *Deployer) deploy(ctx context.Context, out io.Writer, artifact graph.Art
170173 d .portManager .RelinquishPorts (container .Name )
171174 }
172175 if d .cfg .UseCompose {
173- // TODO(nkubala): implement
174- return fmt .Errorf ("docker compose not yet supported by skaffold" )
176+ return d .deployWithCompose (ctx , out , artifact )
175177 }
176178
177179 containerCfg , err := d .containerConfigFromImage (ctx , artifact .Tag )
@@ -352,6 +354,11 @@ func (d *Deployer) Dependencies() ([]string, error) {
352354}
353355
354356func (d * Deployer ) Cleanup (ctx context.Context , out io.Writer , dryRun bool , _ manifest.ManifestListByConfig ) error {
357+ // If using compose, run docker compose down
358+ if d .cfg .UseCompose {
359+ return d .cleanupWithCompose (ctx , out , dryRun )
360+ }
361+
355362 if dryRun {
356363 for _ , container := range d .tracker .DeployedContainers () {
357364 output .Yellow .Fprintln (out , container .ID )
@@ -546,3 +553,137 @@ func (d *Deployer) GetStatusMonitor() status.Monitor {
546553func (d * Deployer ) RegisterLocalImages ([]graph.Artifact ) {
547554 // all images are local, so this is a noop
548555}
556+
557+ // deployWithCompose deploys using docker compose
558+ func (d * Deployer ) deployWithCompose (ctx context.Context , out io.Writer , artifact graph.Artifact ) error {
559+ // Find compose file path (default: docker-compose.yml in current directory)
560+ composeFile := d .getComposeFilePath ()
561+
562+ olog .Entry (ctx ).Infof ("Deploying with docker compose using file: %s" , composeFile )
563+
564+ // Check if compose file exists
565+ if _ , err := os .Stat (composeFile ); err != nil {
566+ return fmt .Errorf ("compose file not found at %s: %w" , composeFile , err )
567+ }
568+
569+ // Read compose file
570+ composeData , err := os .ReadFile (composeFile )
571+ if err != nil {
572+ return fmt .Errorf ("failed to read compose file: %w" , err )
573+ }
574+
575+ // Parse compose file
576+ var composeConfig map [string ]interface {}
577+ if err := yaml .Unmarshal (composeData , & composeConfig ); err != nil {
578+ return fmt .Errorf ("failed to parse compose file: %w" , err )
579+ }
580+
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+ }
585+
586+ // Write modified compose file to temp location
587+ modifiedComposeData , err := yaml .Marshal (composeConfig )
588+ if err != nil {
589+ return fmt .Errorf ("failed to marshal modified compose config: %w" , err )
590+ }
591+
592+ tmpComposeFile , err := os .CreateTemp ("" , "skaffold-compose-*.yml" )
593+ if err != nil {
594+ return fmt .Errorf ("failed to create temporary compose file: %w" , err )
595+ }
596+ defer os .Remove (tmpComposeFile .Name ())
597+ defer tmpComposeFile .Close ()
598+
599+ if _ , err := tmpComposeFile .Write (modifiedComposeData ); err != nil {
600+ return fmt .Errorf ("failed to write temporary compose file: %w" , err )
601+ }
602+ tmpComposeFile .Close ()
603+
604+ // Run docker compose up
605+ args := []string {"compose" , "-f" , tmpComposeFile .Name (), "-p" , fmt .Sprintf ("skaffold-%s" , d .labeller .GetRunID ()), "up" , "-d" }
606+ cmd := exec .CommandContext (ctx , "docker" , args ... )
607+ cmd .Stdout = out
608+ cmd .Stderr = out
609+
610+ olog .Entry (ctx ).Debugf ("Running: docker %v" , strings .Join (args , " " ))
611+
612+ if err := cmd .Run (); err != nil {
613+ return fmt .Errorf ("docker compose up failed: %w" , err )
614+ }
615+
616+ olog .Entry (ctx ).Infof ("Successfully deployed with docker compose" )
617+ return nil
618+ }
619+
620+ // getComposeFilePath returns the path to the docker compose file
621+ func (d * Deployer ) getComposeFilePath () string {
622+ // Check environment variable first
623+ if path := os .Getenv ("SKAFFOLD_COMPOSE_FILE" ); path != "" {
624+ return path
625+ }
626+ // Default to docker-compose.yml in current directory
627+ return "docker-compose.yml"
628+ }
629+
630+ // replaceComposeImages replaces image names in compose config with skaffold-built images
631+ func (d * Deployer ) replaceComposeImages (composeConfig map [string ]interface {}, artifact graph.Artifact ) error {
632+ services , ok := composeConfig ["services" ].(map [string ]interface {})
633+ if ! ok {
634+ return fmt .Errorf ("invalid compose file: services section not found or invalid" )
635+ }
636+
637+ // Iterate through services and replace image if it matches
638+ for serviceName , serviceConfig := range services {
639+ service , ok := serviceConfig .(map [string ]interface {})
640+ if ! ok {
641+ continue
642+ }
643+
644+ // Check if service has an image field
645+ if imageName , ok := service ["image" ].(string ); ok {
646+ // Check if this image matches the artifact's image name
647+ if imageName == artifact .ImageName || strings .Contains (artifact .ImageName , imageName ) {
648+ olog .Entry (context .Background ()).Debugf ("Replacing image for service %s: %s -> %s" , serviceName , imageName , artifact .Tag )
649+ service ["image" ] = artifact .Tag
650+ }
651+ }
652+ }
653+
654+ return nil
655+ }
656+
657+ // cleanupWithCompose cleans up resources deployed with docker compose
658+ func (d * Deployer ) cleanupWithCompose (ctx context.Context , out io.Writer , dryRun bool ) error {
659+ composeFile := d .getComposeFilePath ()
660+ projectName := fmt .Sprintf ("skaffold-%s" , d .labeller .GetRunID ())
661+
662+ if dryRun {
663+ fmt .Fprintf (out , "Would run: docker compose -f %s -p %s down\n " , composeFile , projectName )
664+ return nil
665+ }
666+
667+ olog .Entry (ctx ).Infof ("Cleaning up docker compose deployment" )
668+
669+ // Check if compose file exists
670+ if _ , err := os .Stat (composeFile ); err != nil {
671+ olog .Entry (ctx ).Warnf ("Compose file not found at %s, skipping cleanup" , composeFile )
672+ return nil
673+ }
674+
675+ // Run docker compose down
676+ args := []string {"compose" , "-f" , composeFile , "-p" , projectName , "down" , "--volumes" , "--remove-orphans" }
677+ cmd := exec .CommandContext (ctx , "docker" , args ... )
678+ cmd .Stdout = out
679+ cmd .Stderr = out
680+
681+ olog .Entry (ctx ).Debugf ("Running: docker %v" , strings .Join (args , " " ))
682+
683+ if err := cmd .Run (); err != nil {
684+ return fmt .Errorf ("docker compose down failed: %w" , err )
685+ }
686+
687+ olog .Entry (ctx ).Infof ("Successfully cleaned up docker compose deployment" )
688+ return nil
689+ }
0 commit comments