Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/druid/adapters/cli/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

var pullNoData bool
var pullPreserveReleaseManifest bool

var PullCommand = &cobra.Command{
Use: "pull <artifact> [dir]",
Expand All @@ -21,7 +22,7 @@ var PullCommand = &cobra.Command{

registryClient := registry.NewOciClient(loadRegistryStore())

err := registryClient.PullSelective(dir, artifact, !pullNoData, nil)
err := registryClient.PullSelectiveWithOptions(dir, artifact, !pullNoData, nil, registry.TransferOptions{PreserveReleaseManifest: pullPreserveReleaseManifest})
if err != nil {
logger.Log().Error("Failed to pull from registry")
return err
Expand All @@ -35,4 +36,5 @@ var PullCommand = &cobra.Command{
func init() {
RootCmd.AddCommand(PullCommand)
PullCommand.Flags().BoolVar(&pullNoData, "no-data", false, "Skip scroll data files")
PullCommand.Flags().BoolVar(&pullPreserveReleaseManifest, "preserve-release-manifest", false, "Keep the release manifest.json stored in a backup artifact.")
}
4 changes: 3 additions & 1 deletion apps/druid/adapters/cli/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var pushScrollPorts []string
var pushPackMeta bool
var pushSmart bool
var pushCategory string
var pushPreserveReleaseManifest bool
var pushDisableTarReproducible bool

var PushCommand = &cobra.Command{
Expand Down Expand Up @@ -96,7 +97,7 @@ var PushCommand = &cobra.Command{
overrides[fmt.Sprintf("gg.druid.scroll.port.%s", name)] = port
}

_, err = ociClient.Push(fullPath, repo, tag, overrides, pushPackMeta, &scroll.File)
_, err = ociClient.PushWithOptions(fullPath, repo, tag, overrides, pushPackMeta, &scroll.File, registry.TransferOptions{PreserveReleaseManifest: pushPreserveReleaseManifest})
if err != nil {
return err
}
Expand All @@ -116,5 +117,6 @@ 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 minecraft (Will be added as a manifest annotation gg.druid.scroll.ports.<name>)")
PushCommand.Flags().BoolVarP(&pushPackMeta, "pack-meta", "m", pushPackMeta, "Pack the meta folder into the scroll.")
PushCommand.Flags().BoolVar(&pushPreserveReleaseManifest, "preserve-release-manifest", false, "Include the installed release manifest.json in this snapshot.")
PushCommand.PersistentFlags().BoolVar(&pushDisableTarReproducible, "no-tar-reproducible", false, "Preserve file timestamps in pushed tar layers.")
}
37 changes: 35 additions & 2 deletions internal/core/services/registry/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ func (c *OciClient) Pull(dir string, artifact string) error {
}

func (c *OciClient) PullSelective(dir string, artifact string, includeData bool, progress *domain.SnapshotProgress) error {
return c.PullSelectiveWithOptions(dir, artifact, includeData, progress, TransferOptions{})
}

func (c *OciClient) PullSelectiveWithOptions(dir string, artifact string, includeData bool, progress *domain.SnapshotProgress, options TransferOptions) error {

repo, ref, _ := utils.ParseArtifactRef(artifact)
if repo == "" || ref == "" {
Expand Down Expand Up @@ -334,8 +338,19 @@ func (c *OciClient) PullSelective(dir string, artifact string, includeData bool,

annotations := fullDesc.Annotations
fileName := filepath.Join(dir, "manifest.json")
err = os.WriteFile(fileName, jsonData, 0644)
if err != nil {
if options.PreserveReleaseManifest {
preserved, readErr := os.ReadFile(fileName)
if readErr != nil {
return fmt.Errorf("preserve-release-manifest requires manifest.json in artifact: %w", readErr)
}
var descriptor v1.Descriptor
if err := json.Unmarshal(preserved, &descriptor); err != nil || descriptor.Digest.String() == "" {
if err == nil {
err = fmt.Errorf("descriptor digest is empty")
}
return fmt.Errorf("preserve-release-manifest requires a valid manifest.json in artifact: %w", err)
}
} else if err = os.WriteFile(fileName, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write manifest descriptor: %w", err)
}

Expand Down Expand Up @@ -746,6 +761,10 @@ func copyToRegistry(ctx context.Context, fs *file.Store, repo *remote.Repository
}

func (c *OciClient) Push(folder string, repo string, tag string, overrides map[string]string, packMeta bool, scrollFile *domain.File) (v1.Descriptor, error) {
return c.PushWithOptions(folder, repo, tag, overrides, packMeta, scrollFile, TransferOptions{})
}

func (c *OciClient) PushWithOptions(folder string, repo string, tag string, overrides map[string]string, packMeta bool, scrollFile *domain.File, options TransferOptions) (v1.Descriptor, error) {
ctx := context.Background()

// Authenticate before doing any expensive local work.
Expand Down Expand Up @@ -777,6 +796,20 @@ func (c *OciClient) Push(folder string, repo string, tag string, overrides map[s
if len(fsFileNames) == 0 {
return v1.Descriptor{}, fmt.Errorf("no files found to push")
}
if options.PreserveReleaseManifest {
manifest, err := os.ReadFile(filepath.Join(folder, "manifest.json"))
if err != nil {
return v1.Descriptor{}, fmt.Errorf("preserve-release-manifest requires manifest.json: %w", err)
}
var descriptor v1.Descriptor
if err := json.Unmarshal(manifest, &descriptor); err != nil || descriptor.Digest.String() == "" {
if err == nil {
err = fmt.Errorf("descriptor digest is empty")
}
return v1.Descriptor{}, fmt.Errorf("preserve-release-manifest requires a valid manifest.json: %w", err)
}
fsFileNames = append(fsFileNames, "manifest.json")
}

fs, err := c.newFileStore(folder)
if err != nil {
Expand Down
49 changes: 49 additions & 0 deletions internal/core/services/registry/oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,55 @@ func TestPushPullExecutableDataChunkPreservesMode(t *testing.T) {
}
}

func TestPreserveReleaseManifestRoundTrip(t *testing.T) {
t.Setenv("DRUID_REGISTRY_PLAIN_HTTP", "true")
srv := fakeRegistry(t)
registryHost := strings.TrimPrefix(srv.URL, "http://")
folder := t.TempDir()
if err := os.WriteFile(filepath.Join(folder, "scroll.yaml"), []byte("name: test\nversion: 0.1.0\napp_version: test\n"), 0644); err != nil {
t.Fatal(err)
}
want := []byte(`{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":123}`)
if err := os.WriteFile(filepath.Join(folder, "manifest.json"), want, 0644); err != nil {
t.Fatal(err)
}
client := &OciClient{credentialStore: NewCredentialStore(nil), plainHTTP: true}
repo := registryHost + "/test/preserve-release"
if _, err := client.PushWithOptions(folder, repo, "backup", nil, false, nil, TransferOptions{PreserveReleaseManifest: true}); err != nil {
t.Fatalf("push backup: %v", err)
}
pullDir := t.TempDir()
if err := client.PullSelectiveWithOptions(pullDir, repo+":backup", true, nil, TransferOptions{PreserveReleaseManifest: true}); err != nil {
t.Fatalf("restore backup: %v", err)
}
got, err := os.ReadFile(filepath.Join(pullDir, "manifest.json"))
if err != nil {
t.Fatal(err)
}
if string(got) != string(want) {
t.Fatalf("restored release manifest = %s, want %s", got, want)
}
}

func TestPreserveReleaseManifestRequiresSnapshotDescriptor(t *testing.T) {
t.Setenv("DRUID_REGISTRY_PLAIN_HTTP", "true")
srv := fakeRegistry(t)
registryHost := strings.TrimPrefix(srv.URL, "http://")
folder := t.TempDir()
if err := os.WriteFile(filepath.Join(folder, "scroll.yaml"), []byte("name: test\nversion: 0.1.0\napp_version: test\n"), 0644); err != nil {
t.Fatal(err)
}
client := &OciClient{credentialStore: NewCredentialStore(nil), plainHTTP: true}
repo := registryHost + "/test/missing-release"
if _, err := client.Push(folder, repo, "release", nil, false, nil); err != nil {
t.Fatalf("push normal release: %v", err)
}
err := client.PullSelectiveWithOptions(t.TempDir(), repo+":release", true, nil, TransferOptions{PreserveReleaseManifest: true})
if err == nil || !strings.Contains(err.Error(), "preserve-release-manifest requires manifest.json") {
t.Fatalf("preserving missing descriptor error = %v", err)
}
}

func TestFetchFileReadsScrollYAMLDescriptor(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
Expand Down
8 changes: 8 additions & 0 deletions internal/core/services/registry/transfer_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package registry

// TransferOptions controls whether a runtime release descriptor travels with a
// snapshot. Normal Scroll publication excludes manifest.json because it
// describes a local installation, not the artifact being published.
type TransferOptions struct {
PreserveReleaseManifest bool
}
Loading