diff --git a/cmd/deploy.go b/cmd/deploy.go index 8f2e33d..eb6abd5 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -77,7 +77,15 @@ func deployToServer(project string, cfg *config.Config, server config.Server) er } defer runner.Close() - syncer := imagesync.NewImageSync(imagesync.Config{}, runner) + localStore, err := os.MkdirTemp("", "dockersync-local") + if err != nil { + return fmt.Errorf("failed to create local store: %w", err) + } + + syncer := imagesync.NewImageSync(imagesync.Config{ + LocalStore: localStore, + MaxParallel: 1, + }, runner) deploy := deployment.NewDeployment(runner, syncer) ctx, cancel := context.WithCancel(context.Background()) diff --git a/pkg/deployment/deployment.go b/pkg/deployment/deployment.go index 3c8fb42..f8dac79 100644 --- a/pkg/deployment/deployment.go +++ b/pkg/deployment/deployment.go @@ -28,7 +28,7 @@ type Runner interface { } type ImageSyncer interface { - Sync(ctx context.Context) error + Sync(ctx context.Context, image string) error } type Deployment struct { @@ -361,8 +361,14 @@ func (d *Deployment) startDependency(project string, dependency *config.Dependen } func (d *Deployment) installService(project string, service *config.Service) error { - if _, err := d.pullImage(service.Image); err != nil { - return fmt.Errorf("failed to pull image for %s: %v", service.Image, err) + if service.Image != "" { + if _, err := d.pullImage(service.Image); err != nil { + return fmt.Errorf("failed to pull image for %s: %v", service.Image, err) + } + } else { + if err := d.syncer.Sync(context.Background(), fmt.Sprintf("%s-%s", project, service.Name)); err != nil { + return fmt.Errorf("failed to sync service %s for %s: %v", service.Name, service.Image, err) + } } if err := d.startContainer(project, service, ""); err != nil { @@ -381,8 +387,14 @@ func (d *Deployment) installService(project string, service *config.Service) err func (d *Deployment) updateService(project string, service *config.Service) error { svcName := service.Name - if _, err := d.pullImage(service.Image); err != nil { - return fmt.Errorf("failed to pull new image for %s: %v", svcName, err) + if service.Image != "" { + if _, err := d.pullImage(service.Image); err != nil { + return fmt.Errorf("failed to pull new image for %s: %v", svcName, err) + } + } else { + if err := d.syncer.Sync(context.Background(), fmt.Sprintf("%s-%s", project, service.Name)); err != nil { + return fmt.Errorf("failed to sync service %s for %s: %v", service.Name, service.Image, err) + } } if service.Recreate { @@ -537,7 +549,11 @@ func (d *Deployment) startContainer(project string, service *config.Service, suf args = append(args, "--entrypoint", strings.Join(service.Entrypoint, " ")) } - args = append(args, service.Image) + image := service.Image + if image == "" { + image = fmt.Sprintf("%s-%s", project, service.Name) + } + args = append(args, image) if service.Command != "" { args = append(args, service.Command) diff --git a/pkg/imagesync/imagesync.go b/pkg/imagesync/imagesync.go index 76c8f77..84aee54 100644 --- a/pkg/imagesync/imagesync.go +++ b/pkg/imagesync/imagesync.go @@ -18,7 +18,6 @@ import ( // Config holds the configuration for the Docker image sync operation. type Config struct { - ImageName string LocalStore string RemoteStore string MaxParallel int @@ -46,8 +45,8 @@ func NewImageSync(cfg Config, runner *remote.Runner) *ImageSync { } // Sync performs the Docker image synchronization process. -func (s *ImageSync) Sync(ctx context.Context) error { - needsSync, err := s.compareImages(ctx) +func (s *ImageSync) Sync(ctx context.Context, image string) error { + needsSync, err := s.compareImages(ctx, image) if err != nil { return fmt.Errorf("failed to compare images: %w", err) } @@ -60,19 +59,19 @@ func (s *ImageSync) Sync(ctx context.Context) error { return fmt.Errorf("failed to prepare directories: %w", err) } - if err := s.exportAndExtractImage(ctx); err != nil { + if err := s.exportAndExtractImage(ctx, image); err != nil { return fmt.Errorf("failed to export and extract image: %w", err) } - if err := s.transferMetadata(ctx); err != nil { + if err := s.transferMetadata(ctx, image); err != nil { return fmt.Errorf("failed to transfer metadata: %w", err) } - if err := s.syncBlobs(ctx); err != nil { + if err := s.syncBlobs(ctx, image); err != nil { return fmt.Errorf("failed to sync blobs: %w", err) } - if err := s.loadRemoteImage(ctx); err != nil { + if err := s.loadRemoteImage(ctx, image); err != nil { return fmt.Errorf("failed to load remote image: %w", err) } @@ -80,13 +79,13 @@ func (s *ImageSync) Sync(ctx context.Context) error { } // compareImages checks if the image needs to be synced by comparing local and remote versions. -func (s *ImageSync) compareImages(ctx context.Context) (bool, error) { - localInspect, err := s.inspectLocalImage() +func (s *ImageSync) compareImages(ctx context.Context, image string) (bool, error) { + localInspect, err := s.inspectLocalImage(image) if err != nil { return false, fmt.Errorf("failed to inspect local image: %w", err) } - remoteInspect, err := s.inspectRemoteImage(ctx) + remoteInspect, err := s.inspectRemoteImage(ctx, image) if err != nil { return true, nil // Assume sync needed if remote inspection fails } @@ -126,8 +125,8 @@ type ImageData struct { Os string `json:"Os"` } -func (s *ImageSync) inspectLocalImage() (*ImageData, error) { - cmd := exec.Command("docker", "inspect", s.cfg.ImageName) +func (s *ImageSync) inspectLocalImage(image string) (*ImageData, error) { + cmd := exec.Command("docker", "inspect", image) output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("docker inspect failed: %w", err) @@ -145,8 +144,8 @@ func (s *ImageSync) inspectLocalImage() (*ImageData, error) { return &data[0], nil } -func (s *ImageSync) inspectRemoteImage(ctx context.Context) (*ImageData, error) { - outputReader, err := s.runner.RunCommand(ctx, "docker", "inspect", s.cfg.ImageName) +func (s *ImageSync) inspectRemoteImage(ctx context.Context, image string) (*ImageData, error) { + outputReader, err := s.runner.RunCommand(ctx, "docker", "inspect", image) if err != nil { return nil, err } @@ -181,8 +180,8 @@ func (s *ImageSync) prepareDirectories(ctx context.Context) error { return nil } -func (s *ImageSync) exportAndExtractImage(ctx context.Context) error { - imageDir := normalizeImageName(s.cfg.ImageName) +func (s *ImageSync) exportAndExtractImage(ctx context.Context, image string) error { + imageDir := normalizeImageName(image) localPath := filepath.Join(s.cfg.LocalStore, imageDir) if err := os.MkdirAll(localPath, 0755); err != nil { @@ -190,7 +189,7 @@ func (s *ImageSync) exportAndExtractImage(ctx context.Context) error { } tarPath := filepath.Join(localPath, "image.tar") - cmd := exec.Command("docker", "save", s.cfg.ImageName, "-o", tarPath) + cmd := exec.Command("docker", "save", image, "-o", tarPath) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to save image: %w", err) } @@ -271,13 +270,13 @@ func extractFile(tr *tar.Reader, target string) error { return nil } -func (s *ImageSync) syncBlobs(ctx context.Context) error { - localBlobs, err := s.listLocalBlobs() +func (s *ImageSync) syncBlobs(ctx context.Context, image string) error { + localBlobs, err := s.listLocalBlobs(image) if err != nil { return err } - remoteBlobs, err := s.listRemoteBlobs(ctx) + remoteBlobs, err := s.listRemoteBlobs(ctx, image) if err != nil { return err } @@ -291,10 +290,10 @@ func (s *ImageSync) syncBlobs(ctx context.Context) error { } // Transfer blobs in parallel batches - return s.transferBlobs(ctx, blobsToTransfer) + return s.transferBlobs(ctx, image, blobsToTransfer) } -func (s *ImageSync) transferBlobs(ctx context.Context, blobs []string) error { +func (s *ImageSync) transferBlobs(ctx context.Context, image string, blobs []string) error { if len(blobs) == 0 { return nil } @@ -310,7 +309,7 @@ func (s *ImageSync) transferBlobs(ctx context.Context, blobs []string) error { semaphore <- struct{}{} defer func() { <-semaphore }() - if err := s.transferBlob(ctx, blob); err != nil { + if err := s.transferBlob(ctx, image, blob); err != nil { errChan <- fmt.Errorf("failed to transfer blob %s: %w", blob, err) } }(blob) @@ -330,9 +329,9 @@ func (s *ImageSync) transferBlobs(ctx context.Context, blobs []string) error { return nil } -func (s *ImageSync) loadRemoteImage(ctx context.Context) error { +func (s *ImageSync) loadRemoteImage(ctx context.Context, image string) error { cmd := fmt.Sprintf("cd %s && tar -cf - . | docker load", - filepath.Join(s.cfg.RemoteStore, normalizeImageName(s.cfg.ImageName))) + filepath.Join(s.cfg.RemoteStore, normalizeImageName(image))) outputReader, err := s.runner.RunCommand(ctx, cmd) if err != nil { @@ -374,8 +373,8 @@ func contains(slice []string, item string) bool { } // listLocalBlobs returns a list of blob hashes from the local blob directory. -func (s *ImageSync) listLocalBlobs() ([]string, error) { - imageDir := normalizeImageName(s.cfg.ImageName) +func (s *ImageSync) listLocalBlobs(image string) ([]string, error) { + imageDir := normalizeImageName(image) blobPath := filepath.Join(s.cfg.LocalStore, imageDir, "blobs", "sha256") entries, err := os.ReadDir(blobPath) @@ -394,8 +393,8 @@ func (s *ImageSync) listLocalBlobs() ([]string, error) { } // listRemoteBlobs returns a list of blob hashes from the remote blob directory. -func (s *ImageSync) listRemoteBlobs(ctx context.Context) ([]string, error) { - imageDir := normalizeImageName(s.cfg.ImageName) +func (s *ImageSync) listRemoteBlobs(ctx context.Context, image string) ([]string, error) { + imageDir := normalizeImageName(image) blobPath := filepath.Join(s.cfg.RemoteStore, imageDir, "blobs", "sha256") output, err := s.runner.RunCommand(ctx, "ls", blobPath) @@ -413,8 +412,8 @@ func (s *ImageSync) listRemoteBlobs(ctx context.Context) ([]string, error) { } // transferBlob copies a single blob to the remote host. -func (s *ImageSync) transferBlob(ctx context.Context, blob string) error { - imageDir := normalizeImageName(s.cfg.ImageName) +func (s *ImageSync) transferBlob(ctx context.Context, image string, blob string) error { + imageDir := normalizeImageName(image) localPath := filepath.Join(s.cfg.LocalStore, imageDir, "blobs", "sha256", blob) remotePath := filepath.Join(s.cfg.RemoteStore, imageDir, "blobs", "sha256", blob) @@ -427,8 +426,8 @@ func (s *ImageSync) transferBlob(ctx context.Context, blob string) error { } // transferMetadata copies the image metadata files to the remote host. -func (s *ImageSync) transferMetadata(ctx context.Context) error { - imageDir := normalizeImageName(s.cfg.ImageName) +func (s *ImageSync) transferMetadata(ctx context.Context, image string) error { + imageDir := normalizeImageName(image) localDir := filepath.Join(s.cfg.LocalStore, imageDir) remoteDir := filepath.Join(s.cfg.RemoteStore, imageDir) diff --git a/pkg/imagesync/imagesync_test.go b/pkg/imagesync/imagesync_test.go index 1feabbc..e6ddc09 100644 --- a/pkg/imagesync/imagesync_test.go +++ b/pkg/imagesync/imagesync_test.go @@ -73,7 +73,6 @@ func TestImageSync(t *testing.T) { // Initialize ImageSync cfg := Config{ - ImageName: testImage, LocalStore: localStore, RemoteStore: remoteStore, MaxParallel: 4, @@ -84,7 +83,7 @@ func TestImageSync(t *testing.T) { // Run sync t.Log("Running sync...") ctx := context.Background() - err = sync.Sync(ctx) + err = sync.Sync(ctx, testImage) require.NoError(t, err) // Verify image exists on remote @@ -99,12 +98,12 @@ func TestImageSync(t *testing.T) { // Test image comparison t.Log("Comparing images...") - needsSync, err := sync.compareImages(ctx) + needsSync, err := sync.compareImages(ctx, testImage) require.NoError(t, err) require.False(t, needsSync, "Images should be identical after sync") // Test re-sync with no changes t.Log("Re-syncing...") - err = sync.Sync(ctx) + err = sync.Sync(ctx, testImage) require.NoError(t, err) }