Repository: redhat-openshift-ecosystem/openshift-preflight
Pull request #788 (2022-09-22 18:48:48)

Update .github action versions
I noticed there were a number of GitHub actions that were out of date.

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

This pull-request has been approved by: *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/788#" title="Author self-approved">sebrandon1</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign skattoju for approval by writing `/assign @skattoju` in a comment. For more information see:[The Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

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).

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

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

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":["skattoju"]} -->
openshift-ci[bot]: Hi @sebrandon1. Thanks for your PR.

I'm waiting for a [redhat-openshift-ecosystem](https://github.com/orgs/redhat-openshift-ecosystem/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://github.com/orgs/redhat-openshift-ecosystem/people) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=redhat-openshift-ecosystem%2Fopenshift-preflight).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

coveralls: 
[![Coverage Status](https://coveralls.io/builds/52694006/badge)](https://coveralls.io/builds/52694006)

Coverage remained the same at 84.524% when pulling **21e92be7b4edfab6c7c75ebac61cb99a3da1bf3b on sebrandon1:checkout_update** into **a32ad277d8ce42126b67c68fcb3e17372e9f472d on redhat-openshift-ecosystem:main**.

openshift-ci[bot]: @sebrandon1: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.8-e2e | 21e92be7b4edfab6c7c75ebac61cb99a3da1bf3b | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-openshift-ecosystem_openshift-preflight/788/pull-ci-redhat-openshift-ecosystem-openshift-preflight-main-4.8-e2e/1573023831746940928) | true | `/test 4.8-e2e`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-openshift-ecosystem&repo=openshift-preflight&pr=788). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sebrandon1).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
acornett21: /test 4.8-e2e
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/788#pullrequestreview-1117657910" title="Approved">acornett21</a>*, *<a href="https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/788#" title="Author self-approved">sebrandon1</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)~~ [acornett21]

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]: @sebrandon1: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/4.8-e2e | 21e92be7b4edfab6c7c75ebac61cb99a3da1bf3b | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/redhat-openshift-ecosystem_openshift-preflight/788/pull-ci-redhat-openshift-ecosystem-openshift-preflight-main-4.8-e2e/1573050613179092992) | true | `/test 4.8-e2e`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=redhat-openshift-ecosystem&repo=openshift-preflight&pr=788). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:sebrandon1).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
acornett21: /test 4.8-e2e
dcibot: -  SUCCESS https://www.distributed-ci.io/jobs/b05575e9-9610-477c-8114-c1565f9d92f4/jobStates
 from https://github.com/redhat-openshift-ecosystem/openshift-preflight/pull/788

Patch:
diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml
index ea77659..5853ae3 100644
--- a/.github/workflows/build-main.yml
+++ b/.github/workflows/build-main.yml
@@ -16,7 +16,7 @@ jobs:
         platform: [linux]
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Fetch latest release version
       uses: reloc8/action-latest-release-version@1.0.0
       id: fetch-latest-release
@@ -26,7 +26,7 @@ jobs:
       run: echo SHA_SHORT=$(git rev-parse --short HEAD) >> $GITHUB_ENV
 
     - name: Set up QEMU
-      uses: docker/setup-qemu-action@v1
+      uses: docker/setup-qemu-action@v2
         
     - name: Build Image
       id: build-image
@@ -62,13 +62,13 @@ jobs:
   build-coverage:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
 
     - name: Install system deps
       run: 'sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev'
 
     - name: Set up Go
-      uses: actions/setup-go@v2
+      uses: actions/setup-go@v3
       with:
         go-version: 1.19
 
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index 5d15f7e..15d61bf 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -19,12 +19,12 @@ jobs:
         platform: [linux]
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set Env Tags
       run: echo RELEASE_TAG=$(echo $GITHUB_REF | cut -d '/' -f 3) >> $GITHUB_ENV
 
     - name: Set up QEMU
-      uses: docker/setup-qemu-action@v1
+      uses: docker/setup-qemu-action@v2
 
     - name: Build Image
       id: build-image
diff --git a/.github/workflows/copy-to-rhisv.yml b/.github/workflows/copy-to-rhisv.yml
index b692e78..8f998c6 100644
--- a/.github/workflows/copy-to-rhisv.yml
+++ b/.github/workflows/copy-to-rhisv.yml
@@ -28,7 +28,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
     - name: Set up Go
-      uses: actions/setup-go@v2
+      uses: actions/setup-go@v3
       with:
         go-version: 1.19
 
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index 885264b..1b7b2d3 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -10,7 +10,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Cache build and module paths
       uses: actions/cache@v3
       with:
@@ -25,7 +25,7 @@ jobs:
       run: 'sudo apt update && sudo apt install -y libgpgme-dev libbtrfs-dev libdevmapper-dev'
 
     - name: Set up Go
-      uses: actions/setup-go@v2
+      uses: actions/setup-go@v3
       with:
         go-version: 1.19
 
diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml
index fbd740a..d4ab7ec 100644
--- a/.github/workflows/release-artifacts.yml
+++ b/.github/workflows/release-artifacts.yml
@@ -33,7 +33,7 @@ jobs:
           - s390x
     steps:
     - name: Docker Login
-      uses: docker/login-action@v1
+      uses: docker/login-action@v2
       with:
         registry: ${{ secrets.registry }}
         username: ${{ secrets.user }}
diff --git a/Makefile b/Makefile
index 1040f5e..ba17dad 100644
--- a/Makefile
+++ b/Makefile
@@ -84,7 +84,7 @@ clean:
 	$(shell if [ -f "$(BINARY)-$(GOOS)-$(GOARCH)" ]; then rm -f $(BINARY)-$(GOOS)-$(GOARCH); fi)))
 
 GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint
-GOLANGCI_LINT_VERSION ?= v1.48.0
+GOLANGCI_LINT_VERSION ?= v1.49.0
 golangci-lint: $(GOLANGCI_LINT)
 $(GOLANGCI_LINT):
 	$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION))
diff --git a/README.md b/README.md
index d1de4a0..b41978a 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,12 @@
 [![Build Status](https://github.com/redhat-openshift-ecosystem/openshift-preflight/actions/workflows/go.yml/badge.svg)](https://github.com/redhat-openshift-ecosystem/openshift-preflight/actions?workflow=go)
 [![Coverage Status](https://coveralls.io/repos/github/redhat-openshift-ecosystem/openshift-preflight/badge.svg?branch=main)](https://coveralls.io/github/redhat-openshift-ecosystem/openshift-preflight?branch=main)
 
-**Preflight** is a commandline interface for validating if
-[OpenShift](https://www.openshift.com/) operator bundles and containers meet minimum
-requirements for [Red Hat OpenShift
-Certification](https://connect.redhat.com/en/partner-with-us/red-hat-openshift-certification).
+**Preflight** is a command line (CLI) tool to verify that partner-submitted containers meet minimum requirements for Red Hat Software Certification. These include:
+
+- [OpenShift](https://www.openshift.com) Operator Bundles
+- OpenShift containers
+- [OpenStack](https://www.redhat.com/en/technologies/linux-platforms/openstack-platform) containers
+- [Red Hat Enterprise Linux](https://connect.redhat.com/partner-with-us/red-hat-enterprise-linux-certification) application containers
 
 This project is in active and rapid development! Many facets of this project are
 subject to change, and some features are not fully implemented.
diff --git a/certification/pyxis/pyxis.go b/certification/pyxis/pyxis.go
index ed83490..e47815e 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/submit.go b/certification/pyxis/submit.go
index e1d44de..b390aa9 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.


Test patch:
diff --git a/certification/pyxis/pyxis_suite_test.go b/certification/pyxis/pyxis_suite_test.go
index 7b50771..e431cdd 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_test.go b/certification/pyxis/submit_test.go
index d16d67c..1cd6162 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"