Repository: redhat-openshift-ecosystem/openshift-preflight
Pull request #689 (2022-06-08 17:21:50)

Add FindImagesByDigest to the pyxis client
* Add a method to the Pyxis client that accepts a slice of digests,
  and returns a slice of CertImages.
* So far, it only fills in the ID and Certified fields of the image.

This will be used in the upcoming CsvImagesCertified check.

Signed-off-by: Brad P. Crochet <brad@redhat.com>

openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689#" title="Author self-approved">bcrochet</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/redhat-openshift-ecosystem/openshift-preflight/blob/main/OWNERS)~~ [bcrochet]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/2a4771ff-0d28-4b48-8537-a666d01b27ae/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689
coveralls: 
[![Coverage Status](https://coveralls.io/builds/49853523/badge)](https://coveralls.io/builds/49853523)

Coverage increased (+0.4%) to 60.623% when pulling **da4fee5c4302e60565c297c00a95f0ca680d865a on bcrochet:csv-check** into **1d6894d4c738b293cd463ee69b264c33bae77f5c on redhat-openshift-ecosystem:main**.

dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/4d662894-117d-4870-83f3-e0a4d80e41c3/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689
openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689#" title="Author self-approved">bcrochet</a>*, *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689#pullrequestreview-1000446965" title="LGTM">komish</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/redhat-openshift-ecosystem/openshift-preflight/blob/main/OWNERS)~~ [bcrochet,komish]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
openshift-ci[bot]: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689#" title="Author self-approved">bcrochet</a>*, *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689#pullrequestreview-1001432458" title="Approved">jomkz</a>*, *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/689#pullrequestreview-1000446965" title="LGTM">komish</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/redhat-openshift-ecosystem/openshift-preflight/blob/main/OWNERS)~~ [bcrochet,jomkz,komish]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/certification/pyxis/pyxis.go b/certification/pyxis/pyxis.go
index 410e081..d5ffc4d 100644
--- a/certification/pyxis/pyxis.go
+++ b/certification/pyxis/pyxis.go
@@ -8,6 +8,7 @@ import (
 	"io"
 	"net/http"
 
+	"github.com/shurcooL/graphql"
 	log "github.com/sirupsen/logrus"
 )
 
@@ -126,6 +127,65 @@ func (p *pyxisClient) getImage(ctx context.Context, dockerImageDigest string) (*
 	return &data.Data[0], nil
 }
 
+// FindImagesByDigest uses an unauthenticated call to find_images() graphql function, and will
+// return a slice of CertImages. It accepts a slice of image digests. The query return is then
+// packed into the slice of CertImages.
+func (p *pyxisClient) FindImagesByDigest(ctx context.Context, digests []string) ([]CertImage, error) {
+	if len(digests) == 0 {
+		return nil, fmt.Errorf("no digests specified")
+	}
+	// our graphQL query
+	var query struct {
+		FindImages struct {
+			// Additional fields for return should be added here
+			ContainerImage []struct {
+				ID                graphql.String  `graphql:"_id"`
+				Certified         graphql.Boolean `graphql:"certified"`
+				DockerImageDigest graphql.String  `graphql:"docker_image_digest"`
+			} `graphql:"data"`
+			Error struct {
+				Status graphql.Int    `graphql:"status"`
+				Detail graphql.String `graphql:"detail"`
+			} `graphql:"error"`
+			Total graphql.Int
+			Page  graphql.Int
+			// filter to make sure we get exact results
+		} `graphql:"find_images(filter: {docker_image_digest:in:$digests})"`
+	}
+
+	graphqlDigests := make([]graphql.String, len(digests))
+	for idx, digest := range digests {
+		graphqlDigests[idx] = graphql.String(digest)
+	}
+	// variables to feed to our graphql filter
+	variables := map[string]interface{}{
+		"digests": graphqlDigests,
+	}
+
+	// make our query
+	httpClient, ok := p.Client.(*http.Client)
+	if !ok {
+		return nil, fmt.Errorf("client could not be used as http.Client")
+	}
+	client := graphql.NewClient(p.getPyxisGraphqlUrl(), httpClient)
+
+	err := client.Query(ctx, &query, variables)
+	if err != nil {
+		return nil, fmt.Errorf("error while executing find_images query: %v", err)
+	}
+
+	images := make([]CertImage, len(query.FindImages.ContainerImage))
+	for idx, image := range query.FindImages.ContainerImage {
+		images[idx] = CertImage{
+			ID:                string(image.ID),
+			Certified:         bool(image.Certified),
+			DockerImageDigest: string(image.DockerImageDigest),
+		}
+	}
+
+	return images, nil
+}
+
 func (p *pyxisClient) createRPMManifest(ctx context.Context, rpmManifest *RPMManifest) (*RPMManifest, error) {
 	b, err := json.Marshal(rpmManifest)
 	if err != nil {


Test patch:
diff --git a/certification/pyxis/pyxis_suite_test.go b/certification/pyxis/pyxis_suite_test.go
index dafeaf9..eb509ad 100644
--- a/certification/pyxis/pyxis_suite_test.go
+++ b/certification/pyxis/pyxis_suite_test.go
@@ -41,11 +41,13 @@ func mustWrite(w io.Writer, s string) {
 }
 
 type (
-	pyxisProjectHandler      struct{}
-	pyxisImageHandler        struct{}
-	pyxisRPMManifestHandler  struct{}
-	pyxisTestResultsHandler  struct{}
-	pyxisGraphqlLayerHandler struct{}
+	pyxisProjectHandler           struct{}
+	pyxisImageHandler             struct{}
+	pyxisRPMManifestHandler       struct{}
+	pyxisTestResultsHandler       struct{}
+	pyxisGraphqlLayerHandler      struct{}
+	pyxisGraphqlFindImagesHandler struct{}
+	errorHandler                  struct{}
 )
 
 // For each of these ServeHTTP methods, there is a main switch statement that controls what response will
@@ -150,7 +152,7 @@ func (p *pyxisTestResultsHandler) ServeHTTP(response http.ResponseWriter, reques
 }
 
 func (p *pyxisGraphqlLayerHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
-	log.Trace("In the graphql ServeHTTP")
+	log.Trace("In the graphql Layers ServeHTTP")
 	response.Header().Set("Content-Type", "application/json")
 	if request.Body != nil {
 		defer request.Body.Close()
@@ -180,6 +182,23 @@ func (p *pyxisGraphqlLayerHandler) ServeHTTP(response http.ResponseWriter, reque
 	return
 }
 
+func (p *pyxisGraphqlFindImagesHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
+	log.Trace("In the graphql FindImages ServeHTTP")
+	response.Header().Set("Content-Type", "application/json")
+	if request.Body != nil {
+		defer request.Body.Close()
+	}
+	mustWrite(response, `{"data":{"find_images":{"error":null,"total":1,"page":0,"data":[{"_id":"deadb33f","certified":true}]}}}`)
+}
+
+func (p *errorHandler) ServeHTTP(response http.ResponseWriter, request *http.Request) {
+	response.Header().Set("Content-Type", "application/json")
+	if request.Body != nil {
+		defer request.Body.Close()
+	}
+	response.WriteHeader(http.StatusBadGateway)
+}
+
 // In order to test some negative paths, this io.Reader will just throw an error
 type errReader int
 
diff --git a/certification/pyxis/pyxis_test.go b/certification/pyxis/pyxis_test.go
new file mode 100644
index 0000000..95d10dd
--- /dev/null
+++ b/certification/pyxis/pyxis_test.go
@@ -0,0 +1,41 @@
+package pyxis
+
+import (
+	"context"
+	"net/http"
+
+	. "github.com/onsi/ginkgo/v2/dsl/core"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Pyxis", func() {
+	ctx := context.Background()
+	mux := http.NewServeMux()
+	mux.Handle("/query/", &pyxisGraphqlFindImagesHandler{})
+	pyxisClient := NewPyxisClient("my.pyxis.host/query/", "my-spiffy-api-token", "my-awesome-project-id", &http.Client{Transport: localRoundTripper{handler: mux}})
+
+	Context("Find Images", func() {
+		Context("and one image is passed", func() {
+			It("should return one image", func() {
+				certImages, err := pyxisClient.FindImagesByDigest(ctx, []string{"sha256:deadb33f"})
+				Expect(err).ToNot(HaveOccurred())
+				Expect(certImages).ToNot(BeNil())
+				Expect(certImages).ToNot(BeZero())
+				Expect(certImages[0].Certified).To(BeTrue())
+			})
+		})
+		Context("and an error occurs", func() {
+			It("should return nil and an error", func() {
+				errorMux := http.NewServeMux()
+				errorMux.Handle("/query/", &errorHandler{})
+				pyxisClient.Client = &http.Client{Transport: localRoundTripper{handler: errorMux}}
+				certImages, err := pyxisClient.FindImagesByDigest(ctx, []string{"sha256:dontmatter"})
+				Expect(err).To(HaveOccurred())
+				Expect(certImages).To(BeNil())
+			})
+			AfterEach(func() {
+				pyxisClient.Client = &http.Client{Transport: localRoundTripper{handler: mux}}
+			})
+		})
+	})
+})