diff --git a/apps/druid/adapters/cli/push.go b/apps/druid/adapters/cli/push.go index fb76238..d10b850 100644 --- a/apps/druid/adapters/cli/push.go +++ b/apps/druid/adapters/cli/push.go @@ -22,6 +22,7 @@ var pushScrollPorts []string var pushPackMeta bool var pushSmart bool var pushCategory string +var pushDisableTarReproducible bool var PushCommand = &cobra.Command{ Use: "push [artifact] [dir]", @@ -62,6 +63,9 @@ var PushCommand = &cobra.Command{ logger.Log().Info("Pushing "+repo+":"+tag+" to registry", zap.String("path", fullPath)) ociClient := registry.NewOciClient(credStore) + if pushDisableTarReproducible { + ociClient.DisableTarReproducible() + } overrides := map[string]string{} if pushMinRAM != "" { @@ -112,4 +116,5 @@ func init() { PushCommand.Flags().StringVarP(&pushImage, "image", "i", pushImage, "Image to use for the scroll. (Will be added as a manifest annotation gg.druid.scroll.image)") PushCommand.Flags().StringSliceVarP(&pushScrollPorts, "port", "p", pushScrollPorts, "Ports to expose. Format webserver=80, dns=53/udp or just ftp (Will be added as a manifest annotation gg.druid.scroll.ports.)") PushCommand.Flags().BoolVarP(&pushPackMeta, "pack-meta", "m", pushPackMeta, "Pack the meta folder into the scroll.") + PushCommand.PersistentFlags().BoolVar(&pushDisableTarReproducible, "no-tar-reproducible", false, "Preserve file timestamps in pushed tar layers.") } diff --git a/apps/druid/adapters/cli/push_category.go b/apps/druid/adapters/cli/push_category.go index aee1844..d5b55ae 100644 --- a/apps/druid/adapters/cli/push_category.go +++ b/apps/druid/adapters/cli/push_category.go @@ -26,6 +26,9 @@ var PushCategoryCommand = &cobra.Command{ logger.Log().Info("Pushing "+repo+" category to registry", zap.String("scrollDir", scrollDir)) ociClient := registry.NewOciClient(credStore) + if pushDisableTarReproducible { + ociClient.DisableTarReproducible() + } _, err := ociClient.PushCategory(scrollDir, repo, category) diff --git a/internal/core/services/registry/oci.go b/internal/core/services/registry/oci.go index 47955e9..9d9dbdc 100644 --- a/internal/core/services/registry/oci.go +++ b/internal/core/services/registry/oci.go @@ -40,6 +40,8 @@ type OciClient struct { httpClient *http.Client // plainHTTP forces plain HTTP (no TLS) for registry communication. plainHTTP bool + // disableTarReproducible opts out of reproducible tar layers for pushes. + disableTarReproducible bool } func NewOciClient(credentialStore *CredentialStore) *OciClient { @@ -54,6 +56,20 @@ func plainHTTPFromEnv() bool { return value == "1" || value == "true" || value == "yes" } +// DisableTarReproducible preserves source file timestamps in pushed tar layers. +func (c *OciClient) DisableTarReproducible() { + c.disableTarReproducible = true +} + +func (c *OciClient) newFileStore(root string) (*file.Store, error) { + fs, err := file.New(root) + if err != nil { + return nil, err + } + fs.TarReproducible = !c.disableTarReproducible + return fs, nil +} + func (c *OciClient) GetRepo(repoUrl string) (*remote.Repository, error) { repo, err := remote.NewRepository(repoUrl) if err != nil { @@ -762,7 +778,7 @@ func (c *OciClient) Push(folder string, repo string, tag string, overrides map[s return v1.Descriptor{}, fmt.Errorf("no files found to push") } - fs, err := file.New(folder) + fs, err := c.newFileStore(folder) if err != nil { return v1.Descriptor{}, err } @@ -888,7 +904,7 @@ func (c *OciClient) PushCategory(dir string, repo string, category string) (v1.D return v1.Descriptor{}, err } - fs, err := file.New(dir) + fs, err := c.newFileStore(dir) if err != nil { return v1.Descriptor{}, err } diff --git a/internal/core/services/registry/oci_test.go b/internal/core/services/registry/oci_test.go index b8d70e7..2179b3d 100644 --- a/internal/core/services/registry/oci_test.go +++ b/internal/core/services/registry/oci_test.go @@ -128,6 +128,31 @@ func TestValidateCredentialsUsesPlainHTTPEnv(t *testing.T) { } } +func TestNewFileStoreUsesReproducibleTarsByDefault(t *testing.T) { + client := NewOciClient(NewCredentialStore(nil)) + store, err := client.newFileStore(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer store.Close() + if !store.TarReproducible { + t.Fatal("TarReproducible = false, want true") + } +} + +func TestDisableTarReproducible(t *testing.T) { + client := NewOciClient(NewCredentialStore(nil)) + client.DisableTarReproducible() + store, err := client.newFileStore(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer store.Close() + if store.TarReproducible { + t.Fatal("TarReproducible = true, want false") + } +} + // TestPushDataChunkPathNotDoubled calls OciClient.Push directly with a // relative scroll folder containing a data directory, pushing to a fake // in-process OCI registry. This verifies the data-chunk file paths are