From 82aca791eaab681802a3147a32e95254a3e6c66a Mon Sep 17 00:00:00 2001 From: "Adam D. Cornett" Date: Thu, 15 Sep 2022 14:34:33 -0700 Subject: [PATCH] adding a new updateImage method to enable partners to re-run preflight if there are pyxis errors during our checks Signed-off-by: Adam D. Cornett --- certification/pyxis/pyxis.go | 45 +++++++++++++++++++++++++ certification/pyxis/pyxis_suite_test.go | 11 ++++++ certification/pyxis/submit.go | 16 +++++++++ certification/pyxis/submit_test.go | 40 ++++++++++++++++++++++ 4 files changed, 112 insertions(+) diff --git a/certification/pyxis/pyxis.go b/certification/pyxis/pyxis.go index ed834908..e47815e6 100644 --- a/certification/pyxis/pyxis.go +++ b/certification/pyxis/pyxis.go @@ -127,6 +127,51 @@ func (p *pyxisClient) getImage(ctx context.Context, dockerImageDigest string) (* return &data.Data[0], nil } +// updateImage updates a given certification image based on how the image is built in the `submit` flow +func (p *pyxisClient) updateImage(ctx context.Context, certImage *CertImage) (*CertImage, error) { + // instantiating a patchCertImage struct, so we only send the minimum fields required to pyxis + patchCertImage := &CertImage{ + ID: certImage.ID, + Architecture: certImage.Architecture, + Certified: certImage.Certified, + } + + b, err := json.Marshal(patchCertImage) + if err != nil { + return nil, fmt.Errorf("could not marshal certImage: %w", err) + } + req, err := p.newRequestWithAPIToken(ctx, http.MethodPatch, p.getPyxisURL(fmt.Sprintf("images/id/%s", patchCertImage.ID)), bytes.NewReader(b)) + if err != nil { + return nil, err + } + + resp, err := p.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("cannot update image in pyxis: %w", err) + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("could not read response body: %w", err) + } + + if ok := checkStatus(resp.StatusCode); !ok { + return nil, fmt.Errorf( + "status code: %d: body: %s", + resp.StatusCode, + string(body)) + } + + var updatedCertImage CertImage + if err := json.Unmarshal(body, &updatedCertImage); err != nil { + return nil, fmt.Errorf("could not unmarshal body: %s: %w", string(body), err) + } + + return &updatedCertImage, 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. diff --git a/certification/pyxis/pyxis_suite_test.go b/certification/pyxis/pyxis_suite_test.go index 7b50771c..e431cdd1 100644 --- a/certification/pyxis/pyxis_suite_test.go +++ b/certification/pyxis/pyxis_suite_test.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "testing" . "github.com/onsi/ginkgo/v2/dsl/core" @@ -90,6 +91,14 @@ func (p *pyxisImageHandler) ServeHTTP(response http.ResponseWriter, request *htt responseString := `{"_id":"blah","certified":false,"deleted":false,"image_id":"123456789abc"}` log.Tracef("Method: %s", request.Method) switch { + case request.Method == http.MethodPost && strings.Contains(request.Header["X-Api-Key"][0], "my-update-image"): + response.WriteHeader(http.StatusConflict) + case request.Method == http.MethodGet && strings.Contains(request.Header["X-Api-Key"][0], "my-update-image"): + mustWrite(response, `{"data":[{"_id":"updateImage","certified":false,"deleted":false,"image_id":"123456789abc"}]}`) + case request.Method == http.MethodPatch && request.Header["X-Api-Key"][0] == "my-update-image-success-api-token": + mustWrite(response, `{"_id":"updateImage","certified":true,"deleted":false,"image_id":"123456789abc"}`) + case request.Method == http.MethodPatch && request.Header["X-Api-Key"][0] == "my-update-image-failure-api-token": + response.WriteHeader(http.StatusInternalServerError) case request.Method == http.MethodPost && request.Header["X-Api-Key"][0] == "my-image-409-api-token": response.WriteHeader(http.StatusConflict) case request.Method == http.MethodPost && request.Header["X-Api-Key"][0] == "my-bad-401-image-api-token": @@ -128,6 +137,8 @@ func (p *pyxisRPMManifestHandler) ServeHTTP(response http.ResponseWriter, reques response.WriteHeader(http.StatusUnauthorized) case request.Header["X-Api-Key"][0] == "my-bad-rpmmanifest-api-token": response.WriteHeader(http.StatusUnauthorized) + case request.Method == http.MethodPost && request.Header["X-Api-Key"][0] == "my-update-image-success-api-token": + mustWrite(response, `{"_id":"updateImage"}`) default: mustWrite(response, `{"_id":"blah"}`) } diff --git a/certification/pyxis/submit.go b/certification/pyxis/submit.go index e1d44de4..b390aa92 100644 --- a/certification/pyxis/submit.go +++ b/certification/pyxis/submit.go @@ -55,6 +55,9 @@ func (p *pyxisClient) SubmitResults(ctx context.Context, certInput *Certificatio // in the event that it exists. createImage will wipe it otherwise. originalImageDigest := certImage.DockerImageDigest + // store the certification status for this execution, in case a previous execution failed and we need to patch the image + certified := certInput.CertImage.Certified + // normalizing index.docker.io to docker.io for the certImage certImage.Repositories[0].Registry = normalizeDockerRegistry(certImage.Repositories[0].Registry) @@ -68,6 +71,19 @@ func (p *pyxisClient) SubmitResults(ctx context.Context, certInput *Certificatio if err != nil { return nil, fmt.Errorf("could not get image: %v", err) } + + // checking to see if the original value is certified and the previous value is not certified, + // this would indicate that a partner is running preflight again, and during the first run there was a timeout/error + // in a check that interacts with pyxis and we need to correct the certified value for the image + if certified && !certImage.Certified { + // change the certified value to `true` + certImage.Certified = certified + + certImage, err = p.updateImage(ctx, certImage) + if err != nil { + return nil, fmt.Errorf("could not update image: %v", err) + } + } } // Create the RPM manifest, or get it if it already exists. diff --git a/certification/pyxis/submit_test.go b/certification/pyxis/submit_test.go index d16d67ce..1cd61622 100644 --- a/certification/pyxis/submit_test.go +++ b/certification/pyxis/submit_test.go @@ -19,7 +19,9 @@ var _ = Describe("Pyxis Submit", func() { mux.Handle("/api/v1/projects/certification/id/my-awesome-project-id/test-results", &pyxisTestResultsHandler{}) mux.Handle("/api/v1/projects/certification/id/my-image-project-id/images", &pyxisImageHandler{}) mux.Handle("/api/v1/projects/certification/id/", &pyxisProjectHandler{}) + mux.Handle("/api/v1/images/id/updateImage", &pyxisImageHandler{}) mux.Handle("/api/v1/images/id/blah/", &pyxisRPMManifestHandler{}) + mux.Handle("/api/v1/images/id/updateImage/", &pyxisImageHandler{}) mux.Handle("/api/v1/images", &pyxisImageHandler{}) BeforeEach(func() { @@ -170,6 +172,44 @@ var _ = Describe("Pyxis Submit", func() { }) }) + Context("createImage 409 Conflict, getImage 200, and updateImage 200", func() { + BeforeEach(func() { + pyxisClient.APIToken = "my-update-image-success-api-token" + pyxisClient.ProjectID = "my-image-project-id" + certInput.CertImage.Certified = true + }) + Context("when a project is submitted", func() { + Context("and an update token is sent to getImage and createImage is in conflict", func() { + It("should call updateImage and certified flag should be updated", func() { + certResults, err := pyxisClient.SubmitResults(ctx, &certInput) + Expect(err).ToNot(HaveOccurred()) + Expect(certResults).ToNot(BeNil()) + Expect(certResults.CertProject).ToNot(BeNil()) + Expect(certResults.CertImage).ToNot(BeNil()) + Expect(certResults.CertImage.Certified).To(Equal(true)) + Expect(certResults.TestResults).ToNot(BeNil()) + }) + }) + }) + }) + + Context("createImage 409 Conflict, getImage 200, and updateImage 500", func() { + BeforeEach(func() { + pyxisClient.APIToken = "my-update-image-failure-api-token" + pyxisClient.ProjectID = "my-image-project-id" + certInput.CertImage.Certified = true + }) + Context("when a project is submitted", func() { + Context("and an update token is sent to getImage and createImage is in conflict", func() { + It("should call updateImage and error", func() { + certResults, err := pyxisClient.SubmitResults(ctx, &certInput) + Expect(err).To(HaveOccurred()) + Expect(certResults).To(BeNil()) + }) + }) + }) + }) + Context("createImage 500 InternalServerError", func() { BeforeEach(func() { pyxisClient.APIToken = "my-bad-500-image-api-token"