Repository: regclient/regclient
Pull request #740 (2024-05-12 19:19:50)

Feat: regctl image mod add layer from directory
<!--

Commits must be signed indicating your agreement to the [DCO](https://developercertificate.org/).
See [DCO missing](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) for steps to fix a missing signoff.

-->

### Fixes issue

<!-- If this is a bug fix, include "fixes #xxxx", or "closes #xxxx" -->

### Describe the change

Add the ability to append a layers to an image from a directory. This includes the contents of the directory, including timestamps and file ownership. However, it only includes files and directories, not symlinks or other special files.
<!-- Include the type of change: bug fix, new feature, breaking change, documentation update -->
<!-- Describe what was changed, why the change was made, and how it was implemented -->

### How to verify it

```shell
regctl image mod registry.example.org/repo:v1 --create v1-extended \
  --layer-add "dir=path/to/directory"
```
<!-- Include steps that can be taken to verify the change -->

### Changelog text

- Feat: regctl image mod add layer from directory.
<!-- If the release changelog should have an entry for this, include it here -->

### Please verify and check that the pull request fulfills the following requirements

<!-- Mark the following with an [X] to verify they are included -->

- [X] Tests have been added or not applicable
- [X] Documentation has been added, updated, or not applicable
- [X] Changes have been rebased to main
- [X] Multiple commits to the same code have been squashed

<!-- markdownlint-disable-file MD041 -->



Patch:
diff --git a/cmd/regctl/image.go b/cmd/regctl/image.go
index 8e4be09..ba42a55 100644
--- a/cmd/regctl/image.go
+++ b/cmd/regctl/image.go
@@ -3,6 +3,7 @@ package main
 import (
 	"archive/tar"
 	"bytes"
+	"context"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -265,6 +266,10 @@ regctl image mod alpine:3.5 --to-oci --create registry.example.org/alpine:3.5
 regctl image mod registry.example.org/repo:v1 --create v1-extended \
   --layer-add "tar=file.tar,platform=linux/amd64"
 
+# append a layer to all platforms using the contents of a directory
+regctl image mod registry.example.org/repo:v1 --create v1-extended \
+  --layer-add "dir=path/to/directory"
+
 # set the timestamp on the config and layers, ignoring the alpine base image layers
 regctl image mod registry.example.org/repo:v1 --create v1-mod \
   --time "set=2021-02-03T04:05:06Z,base-ref=alpine:3"
@@ -657,6 +662,23 @@ regctl image ratelimit alpine --format '{{.Remain}}'`,
 					_ = fh.Close()
 				})
 			}
+			if dir, ok := kvSplit["dir"]; ok {
+				if rdr != nil {
+					return fmt.Errorf("cannot use dir and tar options together in layer-add")
+				}
+				pr, pw := io.Pipe()
+				go func() {
+					err := archive.Tar(context.TODO(), dir, pw)
+					if err != nil {
+						_ = pw.CloseWithError(err)
+					}
+					_ = pw.Close()
+				}()
+				rdr = pr
+				cobra.OnFinalize(func() {
+					_ = pr.Close()
+				})
+			}
 			if rdr == nil {
 				return fmt.Errorf("tar file input is required")
 			}
@@ -674,7 +696,7 @@ regctl image ratelimit alpine --format '{{.Remain}}'`,
 				mod.WithLayerAddTar(rdr, mt, platforms))
 			return nil
 		},
-	}, "layer-add", "", `add a new layer (tar=file,platform=val)`)
+	}, "layer-add", "", `add a new layer (tar=file,dir=directory,platform=val)`)
 	imageModCmd.Flags().VarP(&modFlagFunc{
 		t: "string",
 		f: func(val string) error {


Test patch:
diff --git a/cmd/regctl/image_test.go b/cmd/regctl/image_test.go
index 3f93759..9bd6d74 100644
--- a/cmd/regctl/image_test.go
+++ b/cmd/regctl/image_test.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"errors"
 	"fmt"
 	"strings"
 	"testing"
@@ -102,12 +103,52 @@ func TestImageMod(t *testing.T) {
 	srcRef := "ocidir://../../testdata/testrepo:v3"
 	baseRef := "ocidir://../../testdata/testrepo:b1"
 	modRef := fmt.Sprintf("ocidir://%s/repo:mod", tmpDir)
-
-	out, err := cobraTest(t, nil, "image", "mod", srcRef, "--create", modRef, "--time", "set=2000-01-01T00:00:00Z,base-ref="+baseRef)
-	if err != nil {
-		t.Fatalf("failed to run image mod: %v", err)
+	tt := []struct {
+		name        string
+		cmd         []string
+		expectOut   string
+		outContains bool
+		expectErr   error
+	}{
+		{
+			name:      "layer-add-tar",
+			cmd:       []string{"image", "mod", srcRef, "--create", modRef, "--layer-add", "tar=../../testdata/layer.tar,platform=linux/amd64"},
+			expectOut: modRef,
+		},
+		{
+			name:      "layer-add-dir",
+			cmd:       []string{"image", "mod", srcRef, "--create", modRef, "--layer-add", "dir=../../cmd"},
+			expectOut: modRef,
+		},
+		{
+			name:      "layer-add-both",
+			cmd:       []string{"image", "mod", srcRef, "--create", modRef, "--layer-add", "tar=../../testdata/layer.tar,dir=../../cmd,platform=linux/amd64"},
+			expectErr: fmt.Errorf(`invalid argument "tar=../../testdata/layer.tar,dir=../../cmd,platform=linux/amd64" for "--layer-add" flag: cannot use dir and tar options together in layer-add`),
+		},
+		{
+			name:      "timestamps",
+			cmd:       []string{"image", "mod", srcRef, "--create", modRef, "--time", "set=2000-01-01T00:00:00Z,base-ref=" + baseRef},
+			expectOut: modRef,
+		},
 	}
-	if out == "" {
-		t.Errorf("missing output")
+	for _, tc := range tt {
+		t.Run(tc.name, func(t *testing.T) {
+			out, err := cobraTest(t, nil, tc.cmd...)
+			if tc.expectErr != nil {
+				if err == nil {
+					t.Fatalf("command did not fail with expected error: %v", tc.expectErr)
+				}
+				if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
+					t.Fatalf("command failed with unexpected error, expected %v, received %v", tc.expectErr, err)
+				}
+				return
+			}
+			if err != nil {
+				t.Fatalf("command failed with error: %v", err)
+			}
+			if (!tc.outContains && out != tc.expectOut) || (tc.outContains && !strings.Contains(out, tc.expectOut)) {
+				t.Errorf("unexpected output, expected %s, received %s", tc.expectOut, out)
+			}
+		})
 	}
 }