// +build extended package cli //nolint:testpackage import ( "bytes" "context" "encoding/json" "fmt" "io/ioutil" "os" "path" "regexp" "strings" "sync" "gopkg.in/resty.v1" "testing" "time" zotErrors "github.com/anuvu/zot/errors" "github.com/anuvu/zot/pkg/api" "github.com/anuvu/zot/pkg/compliance/v1_0_0" "github.com/anuvu/zot/pkg/extensions" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/phayes/freeport" . "github.com/smartystreets/goconvey/convey" ) const ( BaseURL = "http://127.0.0.1:%s" ) func getBaseURL(port string) string { return fmt.Sprintf(BaseURL, port) } func getFreePort() string { port, err := freeport.GetFreePort() if err != nil { panic(err) } return fmt.Sprint(port) } func TestSearchImageCmd(t *testing.T) { Convey("Test image help", t, func() { args := []string{"--help"} configPath := makeConfigFile("") defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(buff.String(), ShouldContainSubstring, "Usage") So(err, ShouldBeNil) Convey("with the shorthand", func() { args[0] = "-h" configPath := makeConfigFile("") defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(buff.String(), ShouldContainSubstring, "Usage") So(err, ShouldBeNil) }) }) Convey("Test image no url", t, func() { args := []string{"imagetest", "--name", "dummyIdRandom"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) So(err, ShouldEqual, zotErrors.ErrNoURLProvided) }) Convey("Test image no params", t, func() { args := []string{"imagetest", "--url", "someUrl"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldBeNil) }) Convey("Test image invalid url", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "--url", "invalidUrl"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) So(err, ShouldEqual, zotErrors.ErrInvalidURL) So(buff.String(), ShouldContainSubstring, "invalid URL format") }) Convey("Test image invalid url port", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:99999"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) So(buff.String(), ShouldContainSubstring, "invalid port") Convey("without flags", func() { args := []string{"imagetest", "--url", "http://localhost:99999"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldNotBeNil) So(buff.String(), ShouldContainSubstring, "invalid port") }) }) Convey("Test image unreachable", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "--url", "http://localhost:9999"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) }) Convey("Test image url from config", t, func() { args := []string{"imagetest", "--name", "dummyImageName"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB") So(err, ShouldBeNil) }) Convey("Test image by name", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "--url", "someUrlImage"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) imageCmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") imageCmd.SetOut(buff) imageCmd.SetErr(buff) imageCmd.SetArgs(args) err := imageCmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB") So(err, ShouldBeNil) Convey("using shorthand", func() { args := []string{"imagetest", "-n", "dummyImageName", "--url", "someUrlImage"} buff := bytes.NewBufferString("") configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","showspinner":false}]}`) defer os.Remove(configPath) imageCmd := NewImageCommand(new(mockService)) imageCmd.SetOut(buff) imageCmd.SetErr(buff) imageCmd.SetArgs(args) err := imageCmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB") So(err, ShouldBeNil) }) }) } func TestOutputFormat(t *testing.T) { Convey("Test text", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "-o", "text"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, "IMAGE NAME TAG DIGEST SIZE dummyImageName tag DigestsA 123kB") So(err, ShouldBeNil) }) Convey("Test json", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "-o", "json"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, `{ "name": "dummyImageName", "tags": [ { "name":`+ ` "tag", "size": 123445, "digest": "DigestsAreReallyLong", "configDigest": "", "layerDigests": null } ] }`) So(err, ShouldBeNil) }) Convey("Test yaml", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "-o", "yaml"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+ ` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`) So(err, ShouldBeNil) Convey("Test yml", func() { args := []string{"imagetest", "--name", "dummyImageName", "-o", "yml"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, `name: dummyImageName tags: -`+ ` name: tag size: 123445 digest: DigestsAreReallyLong configdigest: "" layers: []`) So(err, ShouldBeNil) }) }) Convey("Test invalid", t, func() { args := []string{"imagetest", "--name", "dummyImageName", "-o", "random"} configPath := makeConfigFile(`{"configs":[{"_name":"imagetest","url":"https://test-url.com","showspinner":false}]}`) defer os.Remove(configPath) cmd := NewImageCommand(new(mockService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err := cmd.Execute() So(err, ShouldNotBeNil) So(buff.String(), ShouldContainSubstring, "invalid output format") }) } func TestServerResponse(t *testing.T) { Convey("Test from real server", t, func() { port := getFreePort() url := getBaseURL(port) config := api.NewConfig() config.HTTP.Port = port config.Extensions = &extensions.ExtensionConfig{ Search: &extensions.SearchConfig{Enable: true}, } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { panic(err) } defer os.RemoveAll(dir) c.Config.Storage.RootDirectory = dir go func(controller *api.Controller) { // this blocks if err := controller.Run(); err != nil { return } }(c) // wait till ready for { _, err := resty.R().Get(url) if err == nil { break } time.Sleep(100 * time.Millisecond) } defer func(controller *api.Controller) { ctx := context.Background() _ = controller.Server.Shutdown(ctx) }(c) uploadManifest(url) Convey("Test all images config url", func() { args := []string{"imagetest"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B") So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B") }) Convey("Test all images verbose", func() { args := []string{"imagetest", "--verbose"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): // IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE // repo7 test:2.0 a0ca253b b8781e88 15B // b8781e88 15B // repo7 test:1.0 a0ca253b b8781e88 15B // b8781e88 15B So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE") So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b b8781e88 15B b8781e88 15B") So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b b8781e88 15B b8781e88 15B") }) Convey("Test image by name config url", func() { args := []string{"imagetest", "--name", "repo7"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B") So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B") Convey("with shorthand", func() { args := []string{"imagetest", "-n", "repo7"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B") So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B") }) }) Convey("Test image by digest", func() { args := []string{"imagetest", "--digest", "a0ca253b"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) // Actual cli output should be something similar to (order of images may differ): // IMAGE NAME TAG DIGEST SIZE // repo7 test:2.0 a0ca253b 15B // repo7 test:1.0 a0ca253b 15B So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B") So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B") Convey("with shorthand", func() { args := []string{"imagetest", "-d", "a0ca253b"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldBeNil) space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B") So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B") }) }) Convey("Test image by name invalid name", func() { args := []string{"imagetest", "--name", "repo777"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) defer os.Remove(configPath) cmd := NewImageCommand(new(searchService)) buff := bytes.NewBufferString("") cmd.SetOut(buff) cmd.SetErr(buff) cmd.SetArgs(args) err = cmd.Execute() So(err, ShouldNotBeNil) actual := buff.String() So(actual, ShouldContainSubstring, "unknown") }) }) } func uploadManifest(url string) { // create a blob/layer resp, _ := resty.R().Post(url + "/v2/repo7/blobs/uploads/") loc := v1_0_0.Location(url, resp) content := []byte("this is a blob5") digest := godigest.FromBytes(content) _, _ = resty.R().SetQueryParam("digest", digest.String()). SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc) // create a manifest m := ispec.Manifest{ Config: ispec.Descriptor{ Digest: digest, Size: int64(len(content)), }, Layers: []ispec.Descriptor{ { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: digest, Size: int64(len(content)), }, }, } m.SchemaVersion = 2 content, _ = json.Marshal(m) _, _ = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). SetBody(content).Put(url + "/v2/repo7/manifests/test:1.0") content = []byte("this is a blob5") digest = godigest.FromBytes(content) // create a manifest with same blob but a different tag m = ispec.Manifest{ Config: ispec.Descriptor{ Digest: digest, Size: int64(len(content)), }, Layers: []ispec.Descriptor{ { MediaType: "application/vnd.oci.image.layer.v1.tar", Digest: digest, Size: int64(len(content)), }, }, } m.SchemaVersion = 2 content, _ = json.Marshal(m) _, _ = resty.R().SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json"). SetBody(content).Put(url + "/v2/repo7/manifests/test:2.0") } type mockService struct{} func (service mockService) getAllImages(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wg *sync.WaitGroup) { defer wg.Done() defer close(channel) image := &imageStruct{} image.Name = "randomimageName" image.Tags = []tags{ { Name: "tag", Digest: "DigestsAreReallyLong", Size: 123445, }, } str, err := image.string(*config.outputFormat) if err != nil { channel <- stringResult{"", err} return } channel <- stringResult{str, nil} } func (service mockService) getImageByName(ctx context.Context, config searchConfig, username, password, imageName string, channel chan stringResult, wg *sync.WaitGroup) { defer wg.Done() defer close(channel) image := &imageStruct{} image.Name = imageName image.Tags = []tags{ { Name: "tag", Digest: "DigestsAreReallyLong", Size: 123445, }, } str, err := image.string(*config.outputFormat) if err != nil { channel <- stringResult{"", err} return } channel <- stringResult{str, nil} } func (service mockService) getCveByImage(ctx context.Context, config searchConfig, username, password, imageName string, c chan stringResult, wg *sync.WaitGroup) { defer wg.Done() defer close(c) cveRes := &cveResult{} cveRes.Data = cveData{ CVEListForImage: cveListForImage{ Tag: imageName, CVEList: []cve{ { ID: "dummyCVEID", Description: "Description of the CVE", Title: "Title of that CVE", Severity: "HIGH", PackageList: []packageList{ { Name: "packagename", FixedVersion: "fixedver", InstalledVersion: "installedver", }, }, }, }, }, } str, err := cveRes.string(*config.outputFormat) if err != nil { c <- stringResult{"", err} return } c <- stringResult{str, nil} } func (service mockService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveID string, c chan stringResult, wg *sync.WaitGroup) { service.getImageByName(ctx, config, username, password, "anImage", c, wg) } func (service mockService) getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string, c chan stringResult, wg *sync.WaitGroup) { service.getImageByName(ctx, config, username, password, "anImage", c, wg) } func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) { service.getImageByName(ctx, config, username, password, imageName, c, wg) } func (service mockService) getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) { service.getImageByName(ctx, config, username, password, imageName, c, wg) } func makeConfigFile(content string) string { os.Setenv("HOME", os.TempDir()) home, err := os.UserHomeDir() if err != nil { panic(err) } configPath := path.Join(home + "/.zot") if err := ioutil.WriteFile(configPath, []byte(content), 0600); err != nil { panic(err) } return configPath }