!80 isula-build:Sync upstream patches
From: @DCCooper Reviewed-by: @jingxiaolu Signed-off-by: @jingxiaolu
This commit is contained in:
commit
9eae1d51d2
@ -1 +1 @@
|
||||
0.9.5-5
|
||||
0.9.5-6
|
||||
|
||||
@ -1 +1 @@
|
||||
4678e505019fae52f1801e172fd19c84ddfc0a70
|
||||
b82408f23540642f79ab000483086997321305bf
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
Name: isula-build
|
||||
Version: 0.9.5
|
||||
Release: 5
|
||||
Release: 6
|
||||
Summary: A tool to build container images
|
||||
License: Mulan PSL V2
|
||||
URL: https://gitee.com/openeuler/isula-build
|
||||
@ -85,6 +85,12 @@ fi
|
||||
/usr/share/bash-completion/completions/isula-build
|
||||
|
||||
%changelog
|
||||
* Wed Mar 03 2021 lixiang <lixiang172@huawei.com> - 0.9.5-6
|
||||
- Type:enhancement
|
||||
- CVE:NA
|
||||
- SUG:restart
|
||||
- DESC:sync patches from upstream
|
||||
|
||||
* Wed Feb 10 2021 lixiang <lixiang172@huawei.com> - 0.9.5-5
|
||||
- Type:enhancement
|
||||
- CVE:NA
|
||||
|
||||
136
patch/0042-fix-some-make-checkall-golangci-lint-flaws.patch
Normal file
136
patch/0042-fix-some-make-checkall-golangci-lint-flaws.patch
Normal file
@ -0,0 +1,136 @@
|
||||
From 34fdae49f82410a8bcc9c6f5940af01a24538de6 Mon Sep 17 00:00:00 2001
|
||||
From: meilier <xingweizheng@huawei.com>
|
||||
Date: Thu, 4 Feb 2021 18:40:31 +0800
|
||||
Subject: [PATCH 01/10] fix some make checkall golangci-lint flaws
|
||||
|
||||
---
|
||||
builder/dockerfile/container/container_src.go | 2 +-
|
||||
builder/dockerfile/container/help.go | 2 +-
|
||||
daemon/load.go | 4 ++--
|
||||
daemon/login.go | 4 ++--
|
||||
daemon/logout.go | 4 ++--
|
||||
daemon/save.go | 2 --
|
||||
pkg/manifest/list.go | 2 +-
|
||||
util/util.go | 2 --
|
||||
8 files changed, 9 insertions(+), 13 deletions(-)
|
||||
|
||||
diff --git a/builder/dockerfile/container/container_src.go b/builder/dockerfile/container/container_src.go
|
||||
index ff52ee2c..9426ec76 100644
|
||||
--- a/builder/dockerfile/container/container_src.go
|
||||
+++ b/builder/dockerfile/container/container_src.go
|
||||
@@ -98,7 +98,7 @@ func (i *containerImageSource) GetBlob(ctx context.Context, blob types.BlobInfo,
|
||||
return nil, -1, errors.Wrapf(err, "blob file %q is not exit", blobFile)
|
||||
}
|
||||
|
||||
- layerFile, err := os.OpenFile(blobFile, os.O_RDONLY, constant.DefaultRootFileMode)
|
||||
+ layerFile, err := os.OpenFile(filepath.Clean(blobFile), os.O_RDONLY, constant.DefaultRootFileMode)
|
||||
if err != nil {
|
||||
return nil, -1, errors.Wrapf(err, "open the blob file %q failed", blobFile)
|
||||
}
|
||||
diff --git a/builder/dockerfile/container/help.go b/builder/dockerfile/container/help.go
|
||||
index c5aa381d..475b479d 100644
|
||||
--- a/builder/dockerfile/container/help.go
|
||||
+++ b/builder/dockerfile/container/help.go
|
||||
@@ -170,7 +170,7 @@ func (ref *Reference) saveLayerToStorage(path string, layer *storage.Layer) (dif
|
||||
}()
|
||||
|
||||
filename := filepath.Join(path, "layer")
|
||||
- layerFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, constant.DefaultRootFileMode)
|
||||
+ layerFile, err := os.OpenFile(filepath.Clean(filename), os.O_CREATE|os.O_WRONLY, constant.DefaultRootFileMode)
|
||||
if err != nil {
|
||||
return "", des, errors.Wrapf(err, "error opening file: %s", filename)
|
||||
}
|
||||
diff --git a/daemon/load.go b/daemon/load.go
|
||||
index 08fb5b1f..d756f9ed 100644
|
||||
--- a/daemon/load.go
|
||||
+++ b/daemon/load.go
|
||||
@@ -55,8 +55,8 @@ func (b *Backend) Load(req *pb.LoadRequest, stream pb.Control_LoadServer) error
|
||||
)
|
||||
opts := b.getLoadOptions(req)
|
||||
|
||||
- if err := util.CheckLoadFile(req.Path); err != nil {
|
||||
- return err
|
||||
+ if cErr := util.CheckLoadFile(req.Path); cErr != nil {
|
||||
+ return cErr
|
||||
}
|
||||
|
||||
repoTags, err = tryToParseImageFormatFromTarball(b.daemon.opts.DataRoot, &opts)
|
||||
diff --git a/daemon/login.go b/daemon/login.go
|
||||
index e3399983..6eeda28e 100644
|
||||
--- a/daemon/login.go
|
||||
+++ b/daemon/login.go
|
||||
@@ -60,8 +60,8 @@ func (b *Backend) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginRes
|
||||
}
|
||||
|
||||
if loginWithAuthFile(req) {
|
||||
- auth, err := config.GetCredentials(sysCtx, req.Server)
|
||||
- if err != nil || auth.Password == "" {
|
||||
+ auth, gErr := config.GetCredentials(sysCtx, req.Server)
|
||||
+ if gErr != nil || auth.Password == "" {
|
||||
auth = types.DockerAuthConfig{}
|
||||
return &pb.LoginResponse{Content: errTryToUseAuth}, errors.Errorf("failed to read auth file: %v", errTryToUseAuth)
|
||||
}
|
||||
diff --git a/daemon/logout.go b/daemon/logout.go
|
||||
index 355b1f7a..d1fbebcb 100644
|
||||
--- a/daemon/logout.go
|
||||
+++ b/daemon/logout.go
|
||||
@@ -47,8 +47,8 @@ func (b *Backend) Logout(ctx context.Context, req *pb.LogoutRequest) (*pb.Logout
|
||||
}
|
||||
|
||||
if req.All {
|
||||
- if err := config.RemoveAllAuthentication(sysCtx); err != nil {
|
||||
- return &pb.LogoutResponse{Result: "Remove authentications failed"}, err
|
||||
+ if rErr := config.RemoveAllAuthentication(sysCtx); rErr != nil {
|
||||
+ return &pb.LogoutResponse{Result: "Remove authentications failed"}, rErr
|
||||
}
|
||||
logrus.Info("Success logout from all servers")
|
||||
|
||||
diff --git a/daemon/save.go b/daemon/save.go
|
||||
index 3dce7bdf..c6411e04 100644
|
||||
--- a/daemon/save.go
|
||||
+++ b/daemon/save.go
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
- "github.com/containers/image/v5/docker/archive"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -34,7 +33,6 @@ import (
|
||||
)
|
||||
|
||||
type saveOptions struct {
|
||||
- writer *archive.Writer
|
||||
sysCtx *types.SystemContext
|
||||
logger *logger.Logger
|
||||
localStore *store.Store
|
||||
diff --git a/pkg/manifest/list.go b/pkg/manifest/list.go
|
||||
index 3f8b2fed..381746f7 100644
|
||||
--- a/pkg/manifest/list.go
|
||||
+++ b/pkg/manifest/list.go
|
||||
@@ -129,7 +129,7 @@ func (l *List) SaveListToImage(store *store.Store, imageID, name string) (string
|
||||
return "", errors.Wrapf(err, "save manifest list to image %v error", imageID)
|
||||
}
|
||||
|
||||
- //marshal list instance information
|
||||
+ // marshal list instance information
|
||||
instancesBytes, err := json.Marshal(&l.instances)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "marshall list instances error")
|
||||
diff --git a/util/util.go b/util/util.go
|
||||
index 61458c73..3f46d796 100644
|
||||
--- a/util/util.go
|
||||
+++ b/util/util.go
|
||||
@@ -57,8 +57,6 @@ const (
|
||||
var (
|
||||
// DefaultRegistryPathPrefix is the map for registry and path
|
||||
DefaultRegistryPathPrefix map[string]string
|
||||
- // clientExporters to map exporter whether will export the image to client
|
||||
- clientExporters map[string]bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
--
|
||||
2.27.0
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
From c0d4159d7719fb94b4b421415b8f367c6f61c68e Mon Sep 17 00:00:00 2001
|
||||
From: DCCooper <1866858@gmail.com>
|
||||
Date: Mon, 8 Feb 2021 10:22:33 +0800
|
||||
Subject: [PATCH 02/10] enhancement: add go test for RUN panic problem
|
||||
|
||||
Signed-off-by: DCCooper <1866858@gmail.com>
|
||||
---
|
||||
builder/dockerfile/parser/parser_test.go | 18 ++++++++++++++----
|
||||
.../testfiles/preprocess/run_with_directive | 2 ++
|
||||
2 files changed, 16 insertions(+), 4 deletions(-)
|
||||
create mode 100644 builder/dockerfile/parser/testfiles/preprocess/run_with_directive
|
||||
|
||||
diff --git a/builder/dockerfile/parser/parser_test.go b/builder/dockerfile/parser/parser_test.go
|
||||
index f0cce1e9..3da5bea6 100644
|
||||
--- a/builder/dockerfile/parser/parser_test.go
|
||||
+++ b/builder/dockerfile/parser/parser_test.go
|
||||
@@ -69,8 +69,9 @@ func TestPreProcess(t *testing.T) {
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
type testcase struct {
|
||||
- name string
|
||||
- expect int
|
||||
+ name string
|
||||
+ expect int
|
||||
+ wantErr bool
|
||||
}
|
||||
var testcases = []testcase{
|
||||
{
|
||||
@@ -89,6 +90,10 @@ func TestFormat(t *testing.T) {
|
||||
name: "yum_config",
|
||||
expect: 8,
|
||||
},
|
||||
+ {
|
||||
+ name: "run_with_directive",
|
||||
+ wantErr: true,
|
||||
+ },
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
@@ -103,8 +108,13 @@ func TestFormat(t *testing.T) {
|
||||
d, err := newDirective(bytes.NewReader(buf.Bytes()))
|
||||
assert.NilError(t, err)
|
||||
lines, err := format(rows, d)
|
||||
- assert.NilError(t, err)
|
||||
- assert.Equal(t, len(lines), tc.expect)
|
||||
+ if (err != nil) != tc.wantErr {
|
||||
+ t.Errorf("Testing failed. Expected: %v, got: %v", tc.wantErr, err)
|
||||
+ }
|
||||
+ if !tc.wantErr {
|
||||
+ assert.NilError(t, err, file)
|
||||
+ assert.Equal(t, len(lines), tc.expect)
|
||||
+ }
|
||||
})
|
||||
}
|
||||
}
|
||||
diff --git a/builder/dockerfile/parser/testfiles/preprocess/run_with_directive b/builder/dockerfile/parser/testfiles/preprocess/run_with_directive
|
||||
new file mode 100644
|
||||
index 00000000..3f3465d3
|
||||
--- /dev/null
|
||||
+++ b/builder/dockerfile/parser/testfiles/preprocess/run_with_directive
|
||||
@@ -0,0 +1,2 @@
|
||||
+FROM scratch
|
||||
+RUN \
|
||||
--
|
||||
2.27.0
|
||||
|
||||
340
patch/0044-fix-load-oci-image-panic.patch
Normal file
340
patch/0044-fix-load-oci-image-panic.patch
Normal file
@ -0,0 +1,340 @@
|
||||
From 947fc1ef0c103f687e195c467ddabd3cf0aa746f Mon Sep 17 00:00:00 2001
|
||||
From: meilier <xingweizheng@huawei.com>
|
||||
Date: Sat, 20 Feb 2021 00:42:55 +0800
|
||||
Subject: [PATCH 06/10] fix load oci image panic
|
||||
|
||||
---
|
||||
cmd/cli/save.go | 3 +
|
||||
cmd/cli/save_test.go | 18 +++++
|
||||
daemon/load.go | 11 +--
|
||||
daemon/load_test.go | 188 +++++++++++++++++++++++++++++++++++--------
|
||||
4 files changed, 181 insertions(+), 39 deletions(-)
|
||||
|
||||
diff --git a/cmd/cli/save.go b/cmd/cli/save.go
|
||||
index 64dc8acc..fe676731 100644
|
||||
--- a/cmd/cli/save.go
|
||||
+++ b/cmd/cli/save.go
|
||||
@@ -72,6 +72,9 @@ func saveCommand(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return errors.New("save accepts at least one image")
|
||||
}
|
||||
+ if saveOpts.format == exporter.OCITransport && len(args) >= 2 {
|
||||
+ return errors.New("oci image format now only supports saving single image")
|
||||
+ }
|
||||
if err := exporter.CheckImageFormat(saveOpts.format); err != nil {
|
||||
return err
|
||||
}
|
||||
diff --git a/cmd/cli/save_test.go b/cmd/cli/save_test.go
|
||||
index 4183aa8b..3fe6bf81 100644
|
||||
--- a/cmd/cli/save_test.go
|
||||
+++ b/cmd/cli/save_test.go
|
||||
@@ -38,6 +38,8 @@ func TestSaveCommand(t *testing.T) {
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
+ // For normal cases, default err is "invalid socket path: unix:///var/run/isula_build.sock".
|
||||
+ // As daemon is not running as we run unit test.
|
||||
var testcases = []testcase{
|
||||
{
|
||||
name: "TC1 - normal case with format docker",
|
||||
@@ -103,6 +105,22 @@ func TestSaveCommand(t *testing.T) {
|
||||
errString: "colon in path",
|
||||
format: "docker",
|
||||
},
|
||||
+ {
|
||||
+ name: "TC9 - normal case save multiple images with format docker",
|
||||
+ path: tmpDir.Join("test9"),
|
||||
+ args: []string{"testImage1", "testImage2"},
|
||||
+ wantErr: true,
|
||||
+ errString: "isula_build.sock",
|
||||
+ format: "docker",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "TC10 - abnormal case save multiple images with format oci",
|
||||
+ path: tmpDir.Join("test10"),
|
||||
+ args: []string{"testImage1", "testImage2"},
|
||||
+ wantErr: true,
|
||||
+ errString: "oci image format now only supports saving single image",
|
||||
+ format: "oci",
|
||||
+ },
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
diff --git a/daemon/load.go b/daemon/load.go
|
||||
index d756f9ed..b557d386 100644
|
||||
--- a/daemon/load.go
|
||||
+++ b/daemon/load.go
|
||||
@@ -147,7 +147,6 @@ func tryToParseImageFormatFromTarball(dataRoot string, opts *loadOptions) ([][]s
|
||||
|
||||
func getDockerRepoTagFromImageTar(systemContext *types.SystemContext, path string) ([][]string, error) {
|
||||
// tmp dir will be removed after NewSourceFromFileWithContext
|
||||
-
|
||||
tarfileSource, err := tarfile.NewSourceFromFileWithContext(systemContext, path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get the source of loading tar file")
|
||||
@@ -168,8 +167,7 @@ func getDockerRepoTagFromImageTar(systemContext *types.SystemContext, path strin
|
||||
|
||||
func getOCIRepoTagFromImageTar(systemContext *types.SystemContext, path string) ([][]string, error) {
|
||||
var (
|
||||
- allRepoTags [][]string
|
||||
- err error
|
||||
+ err error
|
||||
)
|
||||
|
||||
srcRef, err := alltransports.ParseImageName(exporter.FormatTransport(exporter.OCIArchiveTransport, path))
|
||||
@@ -179,14 +177,13 @@ func getOCIRepoTagFromImageTar(systemContext *types.SystemContext, path string)
|
||||
|
||||
tarManifest, err := ociarchive.LoadManifestDescriptorWithContext(systemContext, srcRef)
|
||||
if err != nil {
|
||||
- return nil, errors.Wrapf(err, "failed to loadmanifest descriptor of oci image format")
|
||||
+ return nil, errors.Wrapf(err, "failed to load manifest descriptor of oci image format")
|
||||
}
|
||||
|
||||
- // If index.json has no reference name, compute the image digest instead
|
||||
// For now, we only support load single image in archive file
|
||||
if _, ok := tarManifest.Annotations[imgspecv1.AnnotationRefName]; ok {
|
||||
- allRepoTags = [][]string{{tarManifest.Annotations[imgspecv1.AnnotationRefName]}}
|
||||
+ return [][]string{{tarManifest.Annotations[imgspecv1.AnnotationRefName]}}, nil
|
||||
}
|
||||
|
||||
- return allRepoTags, nil
|
||||
+ return [][]string{{}}, nil
|
||||
}
|
||||
diff --git a/daemon/load_test.go b/daemon/load_test.go
|
||||
index 0513a889..cbcb5d8f 100644
|
||||
--- a/daemon/load_test.go
|
||||
+++ b/daemon/load_test.go
|
||||
@@ -30,6 +30,12 @@ import (
|
||||
"isula.org/isula-build/store"
|
||||
)
|
||||
|
||||
+const (
|
||||
+ loadedTarFile = "load.tar"
|
||||
+ manifestJSONFile = "manifest.json"
|
||||
+ indexJSONFile = "index.json"
|
||||
+)
|
||||
+
|
||||
var (
|
||||
localStore store.Store
|
||||
daemon *Daemon
|
||||
@@ -51,10 +57,10 @@ func (x *controlLoadServer) Context() context.Context {
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
-func prepareLoadTar(dir *fs.Dir) error {
|
||||
- manifest := dir.Join("manifest.json")
|
||||
+func prepareLoadTar(dir *fs.Dir, jsonFile string) error {
|
||||
+ manifest := dir.Join(jsonFile)
|
||||
|
||||
- fi, err := os.Create(dir.Join("load.tar"))
|
||||
+ fi, err := os.Create(dir.Join(loadedTarFile))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -88,9 +94,9 @@ func prepareLoadTar(dir *fs.Dir) error {
|
||||
|
||||
}
|
||||
|
||||
-func prepareForLoad(t *testing.T, manifest string) *fs.Dir {
|
||||
- tmpDir := fs.NewDir(t, t.Name(), fs.WithFile("manifest.json", manifest))
|
||||
- if err := prepareLoadTar(tmpDir); err != nil {
|
||||
+func prepareForLoad(t *testing.T, jsonFile, manifest string) *fs.Dir {
|
||||
+ tmpDir := fs.NewDir(t, t.Name(), fs.WithFile(jsonFile, manifest))
|
||||
+ if err := prepareLoadTar(tmpDir, jsonFile); err != nil {
|
||||
tmpDir.Remove()
|
||||
return nil
|
||||
}
|
||||
@@ -119,34 +125,152 @@ func clean(dir *fs.Dir) {
|
||||
dir.Remove()
|
||||
}
|
||||
|
||||
-func TestLoad(t *testing.T) {
|
||||
- manifestJSON :=
|
||||
- `[
|
||||
- {
|
||||
- "Config":"76a4dd2d5d6a18323ac8d90f959c3c8562bf592e2a559bab9b462ab600e9e5fc.json",
|
||||
- "RepoTags":[
|
||||
- "hello:latest"
|
||||
- ],
|
||||
- "Layers":[
|
||||
- "6eb4c21cc3fcb729a9df230ae522c1d3708ca66e5cf531713dbfa679837aa287.tar",
|
||||
- "37841116ad3b1eeea972c75ab8bad05f48f721a7431924bc547fc91c9076c1c8.tar"
|
||||
- ]
|
||||
+func TestLoadSingleImage(t *testing.T) {
|
||||
+ testcases := []struct {
|
||||
+ name string
|
||||
+ manifest string
|
||||
+ format string
|
||||
+ tarPath string
|
||||
+ withTag bool
|
||||
+ wantErr bool
|
||||
+ errString string
|
||||
+ }{
|
||||
+ {
|
||||
+ name: "TC1 normal case load docker tar",
|
||||
+ manifest: `[
|
||||
+ {
|
||||
+ "Config":"76a4dd2d5d6a18323ac8d90f959c3c8562bf592e2a559bab9b462ab600e9e5fc.json",
|
||||
+ "RepoTags":[
|
||||
+ "hello:latest"
|
||||
+ ],
|
||||
+ "Layers":[
|
||||
+ "6eb4c21cc3fcb729a9df230ae522c1d3708ca66e5cf531713dbfa679837aa287.tar",
|
||||
+ "37841116ad3b1eeea972c75ab8bad05f48f721a7431924bc547fc91c9076c1c8.tar"
|
||||
+ ]
|
||||
+ }
|
||||
+ ]`,
|
||||
+ format: "docker",
|
||||
+ withTag: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "TC2 normal case load oci tar",
|
||||
+ manifest: `{
|
||||
+ "schemaVersion": 2,
|
||||
+ "manifests": [
|
||||
+ {
|
||||
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
+ "digest": "sha256:a65db259a719d915df30c82ce554ab3880ea567e2150d6288580408c2629b802",
|
||||
+ "size": 347,
|
||||
+ "annotations": {
|
||||
+ "org.opencontainers.image.ref.name": "hello:latest"
|
||||
+ }
|
||||
+ }
|
||||
+ ]
|
||||
+ }`,
|
||||
+ format: "oci",
|
||||
+ withTag: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "TC3 normal case load docker tar with no RepoTags",
|
||||
+ manifest: `[
|
||||
+ {
|
||||
+ "Config":"76a4dd2d5d6a18323ac8d90f959c3c8562bf592e2a559bab9b462ab600e9e5fc.json",
|
||||
+ "RepoTags":[],
|
||||
+ "Layers":[
|
||||
+ "6eb4c21cc3fcb729a9df230ae522c1d3708ca66e5cf531713dbfa679837aa287.tar",
|
||||
+ "37841116ad3b1eeea972c75ab8bad05f48f721a7431924bc547fc91c9076c1c8.tar"
|
||||
+ ]
|
||||
+ }
|
||||
+ ]`,
|
||||
+ format: "docker",
|
||||
+ withTag: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "TC4 normal case load oci tar with no annotations",
|
||||
+ manifest: `{
|
||||
+ "schemaVersion": 2,
|
||||
+ "manifests": [
|
||||
+ {
|
||||
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
+ "digest": "sha256:a65db259a719d915df30c82ce554ab3880ea567e2150d6288580408c2629b802",
|
||||
+ "size": 347
|
||||
+ }
|
||||
+ ]
|
||||
+ }`,
|
||||
+ format: "oci",
|
||||
+ withTag: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "TC5 abnormal case load docker tar with wrong manifestJSON",
|
||||
+ manifest: `[
|
||||
+ {
|
||||
+ :"76a4dd2d5d6a18323ac8d90f959c3c8562bf592e2a559bab9b462ab600e9e5fc.json",
|
||||
+ "RepoTags":[
|
||||
+ "hello:latest"
|
||||
+ ],
|
||||
+ "Layers":[
|
||||
+ "6eb4c21cc3fcb729a9df230ae522c1d3708ca66e5cf531713dbfa679837aa287.tar",
|
||||
+ "37841116ad3b1eeea972c75ab8bad05f48f721a7431924bc547fc91c9076c1c8.tar"
|
||||
+ ]
|
||||
+ }
|
||||
+ ]`,
|
||||
+ format: "docker",
|
||||
+ withTag: true,
|
||||
+ wantErr: true,
|
||||
+ errString: "error loading index",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "TC6 abnormal case with wrong tar path",
|
||||
+ manifest: `[
|
||||
+ {
|
||||
+ "Config":"76a4dd2d5d6a18323ac8d90f959c3c8562bf592e2a559bab9b462ab600e9e5fc.json",
|
||||
+ "RepoTags":[
|
||||
+ "hello:latest"
|
||||
+ ],
|
||||
+ "Layers":[
|
||||
+ "6eb4c21cc3fcb729a9df230ae522c1d3708ca66e5cf531713dbfa679837aa287.tar",
|
||||
+ "37841116ad3b1eeea972c75ab8bad05f48f721a7431924bc547fc91c9076c1c8.tar"
|
||||
+ ]
|
||||
+ }
|
||||
+ ]`,
|
||||
+
|
||||
+ tarPath: "/path/that/not/exist/load.tar",
|
||||
+ format: "docker",
|
||||
+ withTag: true,
|
||||
+ wantErr: true,
|
||||
+ errString: "no such file or directory",
|
||||
+ },
|
||||
+ }
|
||||
+
|
||||
+ for _, tc := range testcases {
|
||||
+ t.Run(tc.name, func(t *testing.T) {
|
||||
+ var jsonFile string
|
||||
+ if tc.format == "docker" {
|
||||
+ jsonFile = manifestJSONFile
|
||||
}
|
||||
- ]`
|
||||
- dir := prepareForLoad(t, manifestJSON)
|
||||
- assert.Equal(t, dir != nil, true)
|
||||
- defer clean(dir)
|
||||
+ if tc.format == "oci" {
|
||||
+ jsonFile = indexJSONFile
|
||||
+ }
|
||||
+ dir := prepareForLoad(t, jsonFile, tc.manifest)
|
||||
+ assert.Equal(t, dir != nil, true)
|
||||
+ defer clean(dir)
|
||||
|
||||
- path := dir.Join("load.tar")
|
||||
- repoTags, err := tryToParseImageFormatFromTarball(daemon.opts.DataRoot, &loadOptions{path: path})
|
||||
- assert.NilError(t, err)
|
||||
- assert.Equal(t, repoTags[0][0], "hello:latest")
|
||||
+ path := dir.Join(loadedTarFile)
|
||||
+ if tc.tarPath == "" {
|
||||
+ tc.tarPath = path
|
||||
+ }
|
||||
+ req := &pb.LoadRequest{Path: tc.tarPath}
|
||||
+ stream := &controlLoadServer{}
|
||||
|
||||
- req := &pb.LoadRequest{Path: path}
|
||||
- stream := &controlLoadServer{}
|
||||
+ err := daemon.backend.Load(req, stream)
|
||||
+ if tc.wantErr {
|
||||
+ assert.ErrorContains(t, err, tc.errString)
|
||||
+ return
|
||||
+ }
|
||||
+ assert.ErrorContains(t, err, "failed to get the image")
|
||||
+ })
|
||||
+ }
|
||||
|
||||
- err = daemon.backend.Load(req, stream)
|
||||
- assert.ErrorContains(t, err, "failed to get the image")
|
||||
}
|
||||
|
||||
func TestLoadMultipleImages(t *testing.T) {
|
||||
@@ -181,11 +305,11 @@ func TestLoadMultipleImages(t *testing.T) {
|
||||
]
|
||||
}
|
||||
]`
|
||||
- dir := prepareForLoad(t, manifestJSON)
|
||||
+ dir := prepareForLoad(t, manifestJSONFile, manifestJSON)
|
||||
assert.Equal(t, dir != nil, true)
|
||||
defer clean(dir)
|
||||
|
||||
- path := dir.Join("load.tar")
|
||||
+ path := dir.Join(loadedTarFile)
|
||||
repoTags, err := tryToParseImageFormatFromTarball(daemon.opts.DataRoot, &loadOptions{path: path})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, repoTags[0][0], "registry.example.com/sayhello:first")
|
||||
--
|
||||
2.27.0
|
||||
|
||||
409
patch/0045-fix-images-command-when-only-give-repository.patch
Normal file
409
patch/0045-fix-images-command-when-only-give-repository.patch
Normal file
@ -0,0 +1,409 @@
|
||||
From 4e71f4409e53eadea0aa39383fba3e249072a932 Mon Sep 17 00:00:00 2001
|
||||
From: meilier <xingweizheng@huawei.com>
|
||||
Date: Tue, 2 Feb 2021 00:46:23 +0800
|
||||
Subject: [PATCH 07/10] fix images command when only give repository
|
||||
|
||||
---
|
||||
daemon/images.go | 145 +++++++++++++++++++++-------------
|
||||
daemon/images_test.go | 178 ++++++++++++++++++++++++++++++++++++++++++
|
||||
image/image.go | 9 ++-
|
||||
3 files changed, 277 insertions(+), 55 deletions(-)
|
||||
create mode 100644 daemon/images_test.go
|
||||
|
||||
diff --git a/daemon/images.go b/daemon/images.go
|
||||
index 5560d18c..e61817cc 100644
|
||||
--- a/daemon/images.go
|
||||
+++ b/daemon/images.go
|
||||
@@ -15,9 +15,11 @@ package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
+ "fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
+ "github.com/containers/storage"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
@@ -29,79 +31,114 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
- none = "<none>"
|
||||
- decimalPrefixBase = 1000
|
||||
+ none = "<none>"
|
||||
+ decimalPrefixBase = 1000
|
||||
+ minImageFieldLenWithTag = 2
|
||||
)
|
||||
|
||||
+type listOptions struct {
|
||||
+ localStore *store.Store
|
||||
+ logEntry *logrus.Entry
|
||||
+ imageName string
|
||||
+}
|
||||
+
|
||||
+func (b *Backend) getListOptions(req *pb.ListRequest) listOptions {
|
||||
+ return listOptions{
|
||||
+ localStore: b.daemon.localStore,
|
||||
+ logEntry: logrus.WithFields(logrus.Fields{"ImageName": req.GetImageName()}),
|
||||
+ imageName: req.GetImageName(),
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
// List lists all images
|
||||
func (b *Backend) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) {
|
||||
- logEntry := logrus.WithFields(logrus.Fields{"ImageName": req.GetImageName()})
|
||||
- logEntry.Info("ListRequest received")
|
||||
-
|
||||
- var reqRepository, reqTag string
|
||||
- const minImageFieldLenWithTag = 2
|
||||
- if req.ImageName != "" {
|
||||
- imageName := req.ImageName
|
||||
- _, img, err := image.FindImage(b.daemon.localStore, imageName)
|
||||
- if err != nil {
|
||||
- logEntry.Error(err)
|
||||
- return nil, errors.Wrapf(err, "find local image %v error", imageName)
|
||||
- }
|
||||
+ logrus.WithFields(logrus.Fields{
|
||||
+ "ImageName": req.GetImageName(),
|
||||
+ }).Info("ListRequest received")
|
||||
|
||||
- parts := strings.Split(imageName, ":")
|
||||
- if len(parts) >= minImageFieldLenWithTag {
|
||||
- reqRepository, reqTag = strings.Join(parts[0:len(parts)-1], ":"), parts[len(parts)-1]
|
||||
- }
|
||||
+ opts := b.getListOptions(req)
|
||||
|
||||
- imageInfo := &pb.ListResponse_ImageInfo{
|
||||
- Repository: reqRepository,
|
||||
- Tag: reqTag,
|
||||
- Id: img.ID,
|
||||
- Created: img.Created.Format(constant.LayoutTime),
|
||||
- Size_: getImageSize(b.daemon.localStore, img.ID),
|
||||
- }
|
||||
+ slashLastIndex := strings.LastIndex(opts.imageName, "/")
|
||||
+ colonLastIndex := strings.LastIndex(opts.imageName, ":")
|
||||
+ if opts.imageName != "" && strings.Contains(opts.imageName, ":") && colonLastIndex > slashLastIndex {
|
||||
+ return listOneImage(opts)
|
||||
+ }
|
||||
+ return listImages(opts)
|
||||
+}
|
||||
|
||||
- return &pb.ListResponse{Images: []*pb.ListResponse_ImageInfo{imageInfo}}, nil
|
||||
+func listOneImage(opts listOptions) (*pb.ListResponse, error) {
|
||||
+ _, image, err := image.FindImage(opts.localStore, opts.imageName)
|
||||
+ if err != nil {
|
||||
+ opts.logEntry.Error(err)
|
||||
+ return nil, errors.Wrapf(err, "find local image %v error", opts.imageName)
|
||||
}
|
||||
|
||||
- images, err := b.daemon.localStore.Images()
|
||||
+ result := make([]*pb.ListResponse_ImageInfo, 0, len(image.Names))
|
||||
+ appendImageToResult(&result, image, opts.localStore)
|
||||
+
|
||||
+ for _, info := range result {
|
||||
+ if opts.imageName == fmt.Sprintf("%s:%s", info.Repository, info.Tag) {
|
||||
+ result = []*pb.ListResponse_ImageInfo{info}
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return &pb.ListResponse{Images: result}, nil
|
||||
+}
|
||||
+
|
||||
+func listImages(opts listOptions) (*pb.ListResponse, error) {
|
||||
+ images, err := opts.localStore.Images()
|
||||
if err != nil {
|
||||
- logEntry.Error(err)
|
||||
+ opts.logEntry.Error(err)
|
||||
return &pb.ListResponse{}, errors.Wrap(err, "failed list images from local storage")
|
||||
}
|
||||
+
|
||||
sort.Slice(images, func(i, j int) bool {
|
||||
return images[i].Created.After(images[j].Created)
|
||||
})
|
||||
result := make([]*pb.ListResponse_ImageInfo, 0, len(images))
|
||||
- for _, image := range images {
|
||||
- names := image.Names
|
||||
- if len(names) == 0 {
|
||||
- names = []string{none}
|
||||
+ for i := range images {
|
||||
+ appendImageToResult(&result, &images[i], opts.localStore)
|
||||
+ }
|
||||
+
|
||||
+ if opts.imageName == "" {
|
||||
+ return &pb.ListResponse{Images: result}, nil
|
||||
+ }
|
||||
+
|
||||
+ sameRepositoryResult := make([]*pb.ListResponse_ImageInfo, 0, len(images))
|
||||
+ for _, info := range result {
|
||||
+ if opts.imageName == info.Repository || strings.HasPrefix(info.Id, opts.imageName) {
|
||||
+ sameRepositoryResult = append(sameRepositoryResult, info)
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if len(sameRepositoryResult) == 0 {
|
||||
+ return &pb.ListResponse{}, errors.Errorf("failed to list images with repository %q in local storage", opts.imageName)
|
||||
+ }
|
||||
+ return &pb.ListResponse{Images: sameRepositoryResult}, nil
|
||||
+}
|
||||
+
|
||||
+func appendImageToResult(result *[]*pb.ListResponse_ImageInfo, image *storage.Image, store *store.Store) {
|
||||
+ names := image.Names
|
||||
+ if len(names) == 0 {
|
||||
+ names = []string{none}
|
||||
+ }
|
||||
+
|
||||
+ for _, name := range names {
|
||||
+ repository, tag := name, none
|
||||
+ parts := strings.Split(name, ":")
|
||||
+ if len(parts) >= minImageFieldLenWithTag {
|
||||
+ repository, tag = strings.Join(parts[0:len(parts)-1], ":"), parts[len(parts)-1]
|
||||
}
|
||||
- for _, name := range names {
|
||||
- repository, tag := name, none
|
||||
- parts := strings.Split(name, ":")
|
||||
- if len(parts) >= minImageFieldLenWithTag {
|
||||
- repository, tag = strings.Join(parts[0:len(parts)-1], ":"), parts[len(parts)-1]
|
||||
- }
|
||||
- if reqRepository != "" && reqRepository != repository {
|
||||
- continue
|
||||
- }
|
||||
- if reqTag != "" && reqTag != tag {
|
||||
- continue
|
||||
- }
|
||||
-
|
||||
- imageInfo := &pb.ListResponse_ImageInfo{
|
||||
- Repository: repository,
|
||||
- Tag: tag,
|
||||
- Id: image.ID,
|
||||
- Created: image.Created.Format(constant.LayoutTime),
|
||||
- Size_: getImageSize(b.daemon.localStore, image.ID),
|
||||
- }
|
||||
- result = append(result, imageInfo)
|
||||
+
|
||||
+ imageInfo := &pb.ListResponse_ImageInfo{
|
||||
+ Repository: repository,
|
||||
+ Tag: tag,
|
||||
+ Id: image.ID,
|
||||
+ Created: image.Created.Format(constant.LayoutTime),
|
||||
+ Size_: getImageSize(store, image.ID),
|
||||
}
|
||||
+ *result = append(*result, imageInfo)
|
||||
}
|
||||
- return &pb.ListResponse{Images: result}, nil
|
||||
}
|
||||
|
||||
func getImageSize(store *store.Store, id string) string {
|
||||
diff --git a/daemon/images_test.go b/daemon/images_test.go
|
||||
new file mode 100644
|
||||
index 00000000..a970ce0b
|
||||
--- /dev/null
|
||||
+++ b/daemon/images_test.go
|
||||
@@ -0,0 +1,178 @@
|
||||
+// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
|
||||
+// isula-build licensed under the Mulan PSL v2.
|
||||
+// You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
+// You may obtain a copy of Mulan PSL v2 at:
|
||||
+// http://license.coscl.org.cn/MulanPSL2
|
||||
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
|
||||
+// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
|
||||
+// PURPOSE.
|
||||
+// See the Mulan PSL v2 for more details.
|
||||
+// Author: Weizheng Xing
|
||||
+// Create: 2021-02-03
|
||||
+// Description: This file tests List interface
|
||||
+
|
||||
+package daemon
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "fmt"
|
||||
+ "testing"
|
||||
+
|
||||
+ "github.com/bndr/gotabulate"
|
||||
+ "github.com/containers/storage"
|
||||
+ "github.com/containers/storage/pkg/stringid"
|
||||
+ "gotest.tools/v3/assert"
|
||||
+
|
||||
+ constant "isula.org/isula-build"
|
||||
+ pb "isula.org/isula-build/api/services"
|
||||
+)
|
||||
+
|
||||
+func TestList(t *testing.T) {
|
||||
+ d := prepare(t)
|
||||
+ defer tmpClean(d)
|
||||
+
|
||||
+ options := &storage.ImageOptions{}
|
||||
+ img, err := d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image:test1"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
+ _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image:test2"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
+ _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"egami:test"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
+ // image with no name and tag
|
||||
+ _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
+ d.Daemon.localStore.SetNames(img.ID, append(img.Names, "image:test1-backup"))
|
||||
+ // image who's repo contains port
|
||||
+ _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"hub.example.com:8080/image:test"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
+
|
||||
+ testcases := []struct {
|
||||
+ name string
|
||||
+ req *pb.ListRequest
|
||||
+ wantErr bool
|
||||
+ errString string
|
||||
+ }{
|
||||
+ {
|
||||
+ name: "normal case list specific image with repository[:tag]",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "image:test1",
|
||||
+ },
|
||||
+ wantErr: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case list specific image with image id",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: img.ID,
|
||||
+ },
|
||||
+ wantErr: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case list all images",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "",
|
||||
+ },
|
||||
+ wantErr: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case list all images with repository",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "image",
|
||||
+ },
|
||||
+ wantErr: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "abnormal case no image found in local store",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "coffee:costa",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "failed to parse image",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "abnormal case no repository",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "coffee",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "failed to list images with repository",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "abnormal case ImageName only contains latest tag",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: ":latest",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "invalid reference format",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case ImageName contains port number and tag",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "hub.example.com:8080/image:test",
|
||||
+ },
|
||||
+ wantErr: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case ImageName contains port number",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "hub.example.com:8080/image",
|
||||
+ },
|
||||
+ wantErr: false,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "abnormal case wrong ImageName",
|
||||
+ req: &pb.ListRequest{
|
||||
+ ImageName: "hub.example.com:8080/",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "failed to list images with repository",
|
||||
+ },
|
||||
+ }
|
||||
+
|
||||
+ for _, tc := range testcases {
|
||||
+ t.Run(tc.name, func(t *testing.T) {
|
||||
+ ctx := context.TODO()
|
||||
+ resp, err := d.Daemon.backend.List(ctx, tc.req)
|
||||
+
|
||||
+ if tc.wantErr == true {
|
||||
+ assert.ErrorContains(t, err, tc.errString)
|
||||
+ }
|
||||
+ if tc.wantErr == false {
|
||||
+ assert.NilError(t, err)
|
||||
+ formatAndPrint(resp.Images)
|
||||
+ }
|
||||
+ })
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func formatAndPrint(images []*pb.ListResponse_ImageInfo) {
|
||||
+ emptyStr := `----------- ---- --------- --------
|
||||
+ REPOSITORY TAG IMAGE ID CREATED
|
||||
+ ----------- ---- --------- --------`
|
||||
+ lines := make([][]string, 0, len(images))
|
||||
+ title := []string{"REPOSITORY", "TAG", "IMAGE ID", "CREATED", "SIZE"}
|
||||
+ for _, image := range images {
|
||||
+ if image == nil {
|
||||
+ continue
|
||||
+ }
|
||||
+ line := []string{image.Repository, image.Tag, image.Id[:constant.DefaultIDLen], image.Created, image.Size_}
|
||||
+ lines = append(lines, line)
|
||||
+ }
|
||||
+ if len(lines) == 0 {
|
||||
+ fmt.Println(emptyStr)
|
||||
+ return
|
||||
+ }
|
||||
+ tabulate := gotabulate.Create(lines)
|
||||
+ tabulate.SetHeaders(title)
|
||||
+ tabulate.SetAlign("left")
|
||||
+ fmt.Print(tabulate.Render("simple"))
|
||||
+}
|
||||
diff --git a/image/image.go b/image/image.go
|
||||
index bbbc7b94..36785bdf 100644
|
||||
--- a/image/image.go
|
||||
+++ b/image/image.go
|
||||
@@ -590,12 +590,19 @@ func ResolveName(name string, sc *types.SystemContext, store *store.Store) ([]st
|
||||
}
|
||||
|
||||
func tryResolveNameInStore(name string, store *store.Store) string {
|
||||
+ defaultTag := "latest"
|
||||
+
|
||||
logrus.Infof("Try to find image: %s in local storage", name)
|
||||
img, err := store.Image(name)
|
||||
+ if err == nil {
|
||||
+ return img.ID
|
||||
+ }
|
||||
+
|
||||
+ logrus.Infof("Try to find image: %s:%s in local storage", name, defaultTag)
|
||||
+ img, err = store.Image(fmt.Sprintf("%s:%s", name, defaultTag))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
-
|
||||
return img.ID
|
||||
}
|
||||
|
||||
--
|
||||
2.27.0
|
||||
|
||||
@ -0,0 +1,336 @@
|
||||
From 6ce9d998d0b8e15d7a673626a54477a0bfc9f768 Mon Sep 17 00:00:00 2001
|
||||
From: meilier <xingweizheng@huawei.com>
|
||||
Date: Wed, 3 Feb 2021 01:04:17 +0800
|
||||
Subject: [PATCH 08/10] check if add default tag to image name when using push
|
||||
and save command
|
||||
|
||||
---
|
||||
daemon/pull_test.go | 6 +-
|
||||
daemon/push.go | 6 ++
|
||||
daemon/push_test.go | 13 ++-
|
||||
daemon/save.go | 8 ++
|
||||
daemon/save_test.go | 193 ++++++++++++++++++++++++++++++++++++++++++++
|
||||
image/image.go | 23 ++++++
|
||||
6 files changed, 246 insertions(+), 3 deletions(-)
|
||||
create mode 100644 daemon/save_test.go
|
||||
|
||||
diff --git a/daemon/pull_test.go b/daemon/pull_test.go
|
||||
index 67459d19..27a4d6e8 100644
|
||||
--- a/daemon/pull_test.go
|
||||
+++ b/daemon/pull_test.go
|
||||
@@ -58,7 +58,6 @@ func (c *controlPullServer) Send(response *pb.PullResponse) error {
|
||||
|
||||
func init() {
|
||||
reexec.Init()
|
||||
-
|
||||
}
|
||||
|
||||
func prepare(t *testing.T) daemonTestOptions {
|
||||
@@ -100,7 +99,10 @@ func TestPull(t *testing.T) {
|
||||
defer tmpClean(d)
|
||||
|
||||
options := &storage.ImageOptions{}
|
||||
- d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image:test"}, "", "", options)
|
||||
+ _, err := d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image:test"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
diff --git a/daemon/push.go b/daemon/push.go
|
||||
index e6053dd8..4e3a6ed9 100644
|
||||
--- a/daemon/push.go
|
||||
+++ b/daemon/push.go
|
||||
@@ -63,6 +63,12 @@ func (b *Backend) Push(req *pb.PushRequest, stream pb.Control_PushServer) error
|
||||
return err
|
||||
}
|
||||
|
||||
+ imageName, err := image.CheckAndAddDefaultTag(opt.imageName, opt.localStore)
|
||||
+ if err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ opt.imageName = imageName
|
||||
+
|
||||
manifestType, gErr := exporter.GetManifestType(opt.format)
|
||||
if gErr != nil {
|
||||
return gErr
|
||||
diff --git a/daemon/push_test.go b/daemon/push_test.go
|
||||
index 573e97fe..f4a9e2b1 100644
|
||||
--- a/daemon/push_test.go
|
||||
+++ b/daemon/push_test.go
|
||||
@@ -79,10 +79,21 @@ func TestPush(t *testing.T) {
|
||||
Format: "oci",
|
||||
},
|
||||
},
|
||||
+ {
|
||||
+ name: "manifestNotExist fine without tag latest",
|
||||
+ pushRequest: &pb.PushRequest{
|
||||
+ PushID: stringid.GenerateNonCryptoID()[:constant.DefaultIDLen],
|
||||
+ ImageName: "127.0.0.1/no-repository/no-name",
|
||||
+ Format: "oci",
|
||||
+ },
|
||||
+ },
|
||||
}
|
||||
|
||||
options := &storage.ImageOptions{}
|
||||
- d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"127.0.0.1/no-repository/no-name:latest"}, "", "", options)
|
||||
+ _, err := d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"127.0.0.1/no-repository/no-name:latest"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
diff --git a/daemon/save.go b/daemon/save.go
|
||||
index c6411e04..fd6174b4 100644
|
||||
--- a/daemon/save.go
|
||||
+++ b/daemon/save.go
|
||||
@@ -79,6 +79,14 @@ func (b *Backend) Save(req *pb.SaveRequest, stream pb.Control_SaveServer) error
|
||||
return errors.New("wrong image format provided")
|
||||
}
|
||||
|
||||
+ for i, imageName := range opts.oriImgList {
|
||||
+ nameWithTag, cErr := image.CheckAndAddDefaultTag(imageName, opts.localStore)
|
||||
+ if cErr != nil {
|
||||
+ return cErr
|
||||
+ }
|
||||
+ opts.oriImgList[i] = nameWithTag
|
||||
+ }
|
||||
+
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if rErr := os.Remove(opts.outputPath); rErr != nil && !os.IsNotExist(rErr) {
|
||||
diff --git a/daemon/save_test.go b/daemon/save_test.go
|
||||
new file mode 100644
|
||||
index 00000000..a59086a8
|
||||
--- /dev/null
|
||||
+++ b/daemon/save_test.go
|
||||
@@ -0,0 +1,193 @@
|
||||
+// Copyright (c) Huawei Technologies Co., Ltd. 2020. All rights reserved.
|
||||
+// isula-build licensed under the Mulan PSL v2.
|
||||
+// You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
+// You may obtain a copy of Mulan PSL v2 at:
|
||||
+// http://license.coscl.org.cn/MulanPSL2
|
||||
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
|
||||
+// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
|
||||
+// PURPOSE.
|
||||
+// See the Mulan PSL v2 for more details.
|
||||
+// Author: Weizheng Xing
|
||||
+// Create: 2020-02-03
|
||||
+// Description: This file tests Save interface
|
||||
+
|
||||
+package daemon
|
||||
+
|
||||
+import (
|
||||
+ "context"
|
||||
+ "testing"
|
||||
+
|
||||
+ "github.com/containers/storage"
|
||||
+ "github.com/containers/storage/pkg/reexec"
|
||||
+ "github.com/containers/storage/pkg/stringid"
|
||||
+ "github.com/pkg/errors"
|
||||
+ "golang.org/x/sync/errgroup"
|
||||
+ "google.golang.org/grpc"
|
||||
+ "gotest.tools/v3/assert"
|
||||
+ "gotest.tools/v3/fs"
|
||||
+
|
||||
+ constant "isula.org/isula-build"
|
||||
+ pb "isula.org/isula-build/api/services"
|
||||
+ _ "isula.org/isula-build/exporter/register"
|
||||
+ "isula.org/isula-build/pkg/logger"
|
||||
+)
|
||||
+
|
||||
+type controlSaveServer struct {
|
||||
+ grpc.ServerStream
|
||||
+}
|
||||
+
|
||||
+func (c *controlSaveServer) Context() context.Context {
|
||||
+ return context.Background()
|
||||
+}
|
||||
+
|
||||
+func (c *controlSaveServer) Send(response *pb.SaveResponse) error {
|
||||
+ if response.Log == "error" {
|
||||
+ return errors.New("error happened")
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
+func init() {
|
||||
+ reexec.Init()
|
||||
+}
|
||||
+
|
||||
+func TestSave(t *testing.T) {
|
||||
+ d := prepare(t)
|
||||
+ defer tmpClean(d)
|
||||
+
|
||||
+ //TODO: create image manually and save
|
||||
+ options := &storage.ImageOptions{}
|
||||
+ img, err := d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image:latest"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
+
|
||||
+ _, err = d.Daemon.localStore.CreateImage(stringid.GenerateRandomID(), []string{"image2:test"}, "", "", options)
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("create image with error: %v", err)
|
||||
+ }
|
||||
+
|
||||
+ tempTarfileDir := fs.NewDir(t, t.Name())
|
||||
+ defer tempTarfileDir.Remove()
|
||||
+
|
||||
+ testcases := []struct {
|
||||
+ name string
|
||||
+ req *pb.SaveRequest
|
||||
+ wantErr bool
|
||||
+ errString string
|
||||
+ }{
|
||||
+ {
|
||||
+ name: "normal case save with repository[:tag]",
|
||||
+ req: &pb.SaveRequest{
|
||||
+ SaveID: stringid.GenerateNonCryptoID()[:constant.DefaultIDLen],
|
||||
+ Images: []string{"image:latest"},
|
||||
+ Path: tempTarfileDir.Join("repotag.tar"),
|
||||
+ Format: "docker",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "file does not exist",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case save with repository add default latest",
|
||||
+ req: &pb.SaveRequest{
|
||||
+ SaveID: stringid.GenerateNonCryptoID()[:constant.DefaultIDLen],
|
||||
+ Images: []string{"image"},
|
||||
+ Path: tempTarfileDir.Join("repolatest.tar"),
|
||||
+ Format: "oci",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "file does not exist",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case with imageid",
|
||||
+ req: &pb.SaveRequest{
|
||||
+ SaveID: stringid.GenerateNonCryptoID()[:constant.DefaultIDLen],
|
||||
+ Images: []string{img.ID},
|
||||
+ Path: tempTarfileDir.Join("imageid.tar"),
|
||||
+ Format: "docker",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "file does not exist",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "normal case save multiple images with repository and ID",
|
||||
+ req: &pb.SaveRequest{
|
||||
+ SaveID: stringid.GenerateNonCryptoID()[:constant.DefaultIDLen],
|
||||
+ Images: []string{"image2:test", img.ID},
|
||||
+ Path: tempTarfileDir.Join("double.tar"),
|
||||
+ Format: "docker",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "file does not exist",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "abnormal case save image that not exist in local store",
|
||||
+ req: &pb.SaveRequest{
|
||||
+ SaveID: stringid.GenerateNonCryptoID()[:constant.DefaultIDLen],
|
||||
+ Images: []string{"noexist", img.ID},
|
||||
+ Path: tempTarfileDir.Join("notexist.tar"),
|
||||
+ Format: "docker",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "failed to parse image",
|
||||
+ },
|
||||
+ {
|
||||
+ name: "abnormal case wrong image format",
|
||||
+ req: &pb.SaveRequest{
|
||||
+ SaveID: stringid.GenerateNonCryptoID()[:constant.DefaultIDLen],
|
||||
+ Images: []string{"image", img.ID},
|
||||
+ Path: tempTarfileDir.Join("image.tar"),
|
||||
+ Format: "dock",
|
||||
+ },
|
||||
+ wantErr: true,
|
||||
+ errString: "wrong image format provided",
|
||||
+ },
|
||||
+ }
|
||||
+
|
||||
+ for _, tc := range testcases {
|
||||
+ t.Run(tc.name, func(t *testing.T) {
|
||||
+ stream := &controlSaveServer{}
|
||||
+
|
||||
+ err := d.Daemon.backend.Save(tc.req, stream)
|
||||
+ if tc.wantErr == true {
|
||||
+ assert.ErrorContains(t, err, tc.errString)
|
||||
+ }
|
||||
+ if tc.wantErr == false {
|
||||
+ assert.NilError(t, err)
|
||||
+ }
|
||||
+ })
|
||||
+ }
|
||||
+
|
||||
+}
|
||||
+
|
||||
+func TestSaveHandler(t *testing.T) {
|
||||
+ ctx := context.TODO()
|
||||
+ eg, _ := errgroup.WithContext(ctx)
|
||||
+
|
||||
+ eg.Go(saveHandlerPrint("Push Response"))
|
||||
+ eg.Go(saveHandlerPrint(""))
|
||||
+ eg.Go(saveHandlerPrint("error"))
|
||||
+
|
||||
+ eg.Wait()
|
||||
+}
|
||||
+
|
||||
+func saveHandlerPrint(message string) func() error {
|
||||
+ return func() error {
|
||||
+ stream := &controlSaveServer{}
|
||||
+ cliLogger := logger.NewCliLogger(constant.CliLogBufferLen)
|
||||
+
|
||||
+ ctx := context.TODO()
|
||||
+ eg, _ := errgroup.WithContext(ctx)
|
||||
+
|
||||
+ eg.Go(messageHandler(stream, cliLogger))
|
||||
+ eg.Go(func() error {
|
||||
+ cliLogger.Print(message)
|
||||
+ cliLogger.CloseContent()
|
||||
+ return nil
|
||||
+ })
|
||||
+
|
||||
+ eg.Wait()
|
||||
+
|
||||
+ return nil
|
||||
+ }
|
||||
+}
|
||||
diff --git a/image/image.go b/image/image.go
|
||||
index 36785bdf..1e480391 100644
|
||||
--- a/image/image.go
|
||||
+++ b/image/image.go
|
||||
@@ -689,3 +689,26 @@ func tryResolveNameInRegistries(name string, sc *types.SystemContext) ([]string,
|
||||
}
|
||||
return candidates, exporter.DockerTransport
|
||||
}
|
||||
+
|
||||
+// CheckAndAddDefaultTag checks if src is format of repository[:tag], add default tag if src without tag
|
||||
+func CheckAndAddDefaultTag(imageName string, store *store.Store) (string, error) {
|
||||
+ _, img, err := FindImage(store, imageName)
|
||||
+ if err != nil {
|
||||
+ return "", errors.Wrapf(err, "find src image: %q failed", imageName)
|
||||
+ }
|
||||
+
|
||||
+ defaultTag := "latest"
|
||||
+ for _, name := range img.Names {
|
||||
+ // for imageName is the format of repository[:tag]
|
||||
+ if imageName == name {
|
||||
+ return imageName, nil
|
||||
+ }
|
||||
+ // for name is the format of repository
|
||||
+ if fmt.Sprintf("%s:%s", imageName, defaultTag) == name {
|
||||
+ return name, nil
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // for imageName is the format of imageID
|
||||
+ return imageName, nil
|
||||
+}
|
||||
--
|
||||
2.27.0
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
From 04dc1756a397edcf99caffd22a85902973a7c40f Mon Sep 17 00:00:00 2001
|
||||
From: meilier <xingweizheng@huawei.com>
|
||||
Date: Wed, 3 Feb 2021 01:05:37 +0800
|
||||
Subject: [PATCH 09/10] checkAndExpandTag return empty when tag is empty
|
||||
|
||||
---
|
||||
builder/dockerfile/builder.go | 2 +-
|
||||
builder/dockerfile/builder_test.go | 2 +-
|
||||
daemon/import.go | 4 +++-
|
||||
3 files changed, 5 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go
|
||||
index f860e60b..42229746 100644
|
||||
--- a/builder/dockerfile/builder.go
|
||||
+++ b/builder/dockerfile/builder.go
|
||||
@@ -637,7 +637,7 @@ func parseOutputTag(output string) string {
|
||||
// CheckAndExpandTag checks tag name. If it not include a tag, "latest" will be added.
|
||||
func CheckAndExpandTag(tag string) (reference.Named, string, error) {
|
||||
if tag == "" {
|
||||
- return nil, "<none>:<none>", nil
|
||||
+ return nil, "", nil
|
||||
}
|
||||
|
||||
newTag := tag
|
||||
diff --git a/builder/dockerfile/builder_test.go b/builder/dockerfile/builder_test.go
|
||||
index f8de41f1..3b7513be 100644
|
||||
--- a/builder/dockerfile/builder_test.go
|
||||
+++ b/builder/dockerfile/builder_test.go
|
||||
@@ -1533,7 +1533,7 @@ func TestCheckAndExpandTag(t *testing.T) {
|
||||
{
|
||||
name: "test 9",
|
||||
tag: "",
|
||||
- output: "<none>:<none>",
|
||||
+ output: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
diff --git a/daemon/import.go b/daemon/import.go
|
||||
index 21ffeaa3..3d7c0d03 100644
|
||||
--- a/daemon/import.go
|
||||
+++ b/daemon/import.go
|
||||
@@ -121,7 +121,9 @@ func (b *Backend) Import(req *pb.ImportRequest, stream pb.Control_ImportServer)
|
||||
return errors.Wrapf(err, "error locating image %q in local storage after import", transports.ImageName(dstRef))
|
||||
}
|
||||
imageID = img.ID
|
||||
- img.Names = append(img.Names, reference)
|
||||
+ if reference != "" {
|
||||
+ img.Names = append(img.Names, reference)
|
||||
+ }
|
||||
newNames := util.CopyStringsWithoutSpecificElem(img.Names, tmpName)
|
||||
if err = localStore.SetNames(img.ID, newNames); err != nil {
|
||||
return errors.Wrapf(err, "failed to prune temporary name from image %q", imageID)
|
||||
--
|
||||
2.27.0
|
||||
|
||||
@ -0,0 +1,111 @@
|
||||
From af2e9918063d2797ba9f16306a4e7d2bbb0e85f7 Mon Sep 17 00:00:00 2001
|
||||
From: DCCooper <1866858@gmail.com>
|
||||
Date: Tue, 2 Mar 2021 15:48:52 +0800
|
||||
Subject: [PATCH 10/10] trim space when counting length of fields to avoid
|
||||
panic
|
||||
|
||||
Signed-off-by: DCCooper <1866858@gmail.com>
|
||||
---
|
||||
builder/dockerfile/parser/parser.go | 2 +-
|
||||
builder/dockerfile/parser/parser_test.go | 22 ++++++++++++++++++-
|
||||
.../testfiles/preprocess/cmd_with_directive | 2 ++
|
||||
.../preprocess/cmd_with_directive_with_space | 2 ++
|
||||
.../preprocess/entrypoint_with_directive | 2 ++
|
||||
.../entrypoint_with_directive_with_space | 2 ++
|
||||
.../preprocess/run_with_directive_with_space | 2 ++
|
||||
7 files changed, 32 insertions(+), 2 deletions(-)
|
||||
create mode 100644 builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive
|
||||
create mode 100644 builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive_with_space
|
||||
create mode 100644 builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive
|
||||
create mode 100644 builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive_with_space
|
||||
create mode 100644 builder/dockerfile/parser/testfiles/preprocess/run_with_directive_with_space
|
||||
|
||||
diff --git a/builder/dockerfile/parser/parser.go b/builder/dockerfile/parser/parser.go
|
||||
index 3caa516a..a21a3f59 100644
|
||||
--- a/builder/dockerfile/parser/parser.go
|
||||
+++ b/builder/dockerfile/parser/parser.go
|
||||
@@ -161,7 +161,7 @@ func format(rows []*rowLine, d *directive) ([]*parser.Line, error) {
|
||||
fields := strings.SplitN(logicLine, " ", 2)
|
||||
const validLineLen = 2
|
||||
// we do not allow empty raw command been passed
|
||||
- if len(fields) < validLineLen || len(fields[1]) == 0 {
|
||||
+ if len(fields) < validLineLen || len(strings.TrimSpace(fields[1])) == 0 {
|
||||
return nil, errors.Errorf("line %q should have at least two fields", logicLine)
|
||||
}
|
||||
line.Command = strings.ToUpper(fields[0])
|
||||
diff --git a/builder/dockerfile/parser/parser_test.go b/builder/dockerfile/parser/parser_test.go
|
||||
index 3da5bea6..870132f1 100644
|
||||
--- a/builder/dockerfile/parser/parser_test.go
|
||||
+++ b/builder/dockerfile/parser/parser_test.go
|
||||
@@ -91,7 +91,27 @@ func TestFormat(t *testing.T) {
|
||||
expect: 8,
|
||||
},
|
||||
{
|
||||
- name: "run_with_directive",
|
||||
+ name: "run_with_directive",
|
||||
+ wantErr: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "run_with_directive_with_space",
|
||||
+ wantErr: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "cmd_with_directive",
|
||||
+ wantErr: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "cmd_with_directive_with_space",
|
||||
+ wantErr: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "entrypoint_with_directive",
|
||||
+ wantErr: true,
|
||||
+ },
|
||||
+ {
|
||||
+ name: "entrypoint_with_directive_with_space",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
diff --git a/builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive b/builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive
|
||||
new file mode 100644
|
||||
index 00000000..545c278c
|
||||
--- /dev/null
|
||||
+++ b/builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive
|
||||
@@ -0,0 +1,2 @@
|
||||
+FROM scratch
|
||||
+CMD \
|
||||
diff --git a/builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive_with_space b/builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive_with_space
|
||||
new file mode 100644
|
||||
index 00000000..fc309502
|
||||
--- /dev/null
|
||||
+++ b/builder/dockerfile/parser/testfiles/preprocess/cmd_with_directive_with_space
|
||||
@@ -0,0 +1,2 @@
|
||||
+FROM scratch
|
||||
+CMD \
|
||||
diff --git a/builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive b/builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive
|
||||
new file mode 100644
|
||||
index 00000000..59369bea
|
||||
--- /dev/null
|
||||
+++ b/builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive
|
||||
@@ -0,0 +1,2 @@
|
||||
+FROM scratch
|
||||
+ENTRYPOINT \
|
||||
diff --git a/builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive_with_space b/builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive_with_space
|
||||
new file mode 100644
|
||||
index 00000000..172aa714
|
||||
--- /dev/null
|
||||
+++ b/builder/dockerfile/parser/testfiles/preprocess/entrypoint_with_directive_with_space
|
||||
@@ -0,0 +1,2 @@
|
||||
+FROM scratch
|
||||
+ENTRYPOINT \
|
||||
diff --git a/builder/dockerfile/parser/testfiles/preprocess/run_with_directive_with_space b/builder/dockerfile/parser/testfiles/preprocess/run_with_directive_with_space
|
||||
new file mode 100644
|
||||
index 00000000..c742c4c3
|
||||
--- /dev/null
|
||||
+++ b/builder/dockerfile/parser/testfiles/preprocess/run_with_directive_with_space
|
||||
@@ -0,0 +1,2 @@
|
||||
+FROM scratch
|
||||
+RUN \
|
||||
--
|
||||
2.27.0
|
||||
|
||||
@ -5,3 +5,10 @@ patch/0037-isula-build-fix-goroutine-leak-problem.patch
|
||||
patch/0039-bugfix-remove-Healthcheck-field-when-build-from-scra.patch
|
||||
patch/0040-vendor-update-tabulate-vendor-to-support-eliminate-s.patch
|
||||
patch/0041-enhancement-remove-empty-lines-when-showing-image-li.patch
|
||||
patch/0042-fix-some-make-checkall-golangci-lint-flaws.patch
|
||||
patch/0043-enhancement-add-go-test-for-RUN-panic-problem.patch
|
||||
patch/0044-fix-load-oci-image-panic.patch
|
||||
patch/0045-fix-images-command-when-only-give-repository.patch
|
||||
patch/0046-check-if-add-default-tag-to-image-name-when-using-pu.patch
|
||||
patch/0047-checkAndExpandTag-return-empty-when-tag-is-empty.patch
|
||||
patch/0048-trim-space-when-counting-length-of-fields-to-avoid-p.patch
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user