diff --git a/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json b/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json index 7978faceb..e6a465931 100644 --- a/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json +++ b/openshift/tests-extension/.openshift-tests-extension/openshift_payload_olmv1.json @@ -177,6 +177,22 @@ "exclude": "topology==\"External\"" } }, + { + "name": "[sig-olmv1][Jira:OLM] clustercatalog PolarionID:85889-[OTP][Skipped:Disconnected]Validate catalogd content via port-forward [Serial]", + "labels": { + "ClusterCatalog": {}, + "Extended": {}, + "NonHyperShiftHOST": {} + }, + "resources": { + "isolation": {} + }, + "source": "openshift:payload:olmv1", + "lifecycle": "blocking", + "environmentSelector": { + "exclude": "topology==\"External\"" + } + }, { "name": "[sig-olmv1][Jira:OLM] clustercatalog PolarionID:73219-[OTP][Skipped:Disconnected]Fetch deprecation data from the catalogd http server", "labels": { diff --git a/openshift/tests-extension/pkg/bindata/qe/bindata.go b/openshift/tests-extension/pkg/bindata/qe/bindata.go index bccc3a73e..69de763d3 100644 --- a/openshift/tests-extension/pkg/bindata/qe/bindata.go +++ b/openshift/tests-extension/pkg/bindata/qe/bindata.go @@ -7,6 +7,7 @@ // test/qe/testdata/olm/cip.yaml // test/qe/testdata/olm/clustercatalog-secret-withlabel.yaml // test/qe/testdata/olm/clustercatalog-secret.yaml +// test/qe/testdata/olm/clustercatalog-with-pollinterval.yaml // test/qe/testdata/olm/clustercatalog-withlabel.yaml // test/qe/testdata/olm/clustercatalog.yaml // test/qe/testdata/olm/clusterextension-withselectorExpressions-WithoutChannelVersion.yaml @@ -415,6 +416,45 @@ func testQeTestdataOlmClustercatalogSecretYaml() (*asset, error) { return a, nil } +var _testQeTestdataOlmClustercatalogWithPollintervalYaml = []byte(`apiVersion: template.openshift.io/v1 +kind: Template +metadata: + name: catalog-template +objects: +- apiVersion: olm.operatorframework.io/v1 + kind: ClusterCatalog + metadata: + name: "${NAME}" + spec: + source: + type: "${TYPE}" + image: + ref: "${IMAGE}" + pollInterval: "${POLLINTERVALMINUTES}" +parameters: +- name: NAME +- name: TYPE + value: "Image" +- name: IMAGE +- name: POLLINTERVALMINUTES + value: "300s" +`) + +func testQeTestdataOlmClustercatalogWithPollintervalYamlBytes() ([]byte, error) { + return _testQeTestdataOlmClustercatalogWithPollintervalYaml, nil +} + +func testQeTestdataOlmClustercatalogWithPollintervalYaml() (*asset, error) { + bytes, err := testQeTestdataOlmClustercatalogWithPollintervalYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "test/qe/testdata/olm/clustercatalog-with-pollinterval.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _testQeTestdataOlmClustercatalogWithlabelYaml = []byte(`apiVersion: template.openshift.io/v1 kind: Template metadata: @@ -2315,6 +2355,7 @@ var _bindata = map[string]func() (*asset, error){ "test/qe/testdata/olm/cip.yaml": testQeTestdataOlmCipYaml, "test/qe/testdata/olm/clustercatalog-secret-withlabel.yaml": testQeTestdataOlmClustercatalogSecretWithlabelYaml, "test/qe/testdata/olm/clustercatalog-secret.yaml": testQeTestdataOlmClustercatalogSecretYaml, + "test/qe/testdata/olm/clustercatalog-with-pollinterval.yaml": testQeTestdataOlmClustercatalogWithPollintervalYaml, "test/qe/testdata/olm/clustercatalog-withlabel.yaml": testQeTestdataOlmClustercatalogWithlabelYaml, "test/qe/testdata/olm/clustercatalog.yaml": testQeTestdataOlmClustercatalogYaml, "test/qe/testdata/olm/clusterextension-withselectorExpressions-WithoutChannelVersion.yaml": testQeTestdataOlmClusterextensionWithselectorexpressionsWithoutchannelversionYaml, @@ -2388,15 +2429,16 @@ var _bintree = &bintree{nil, map[string]*bintree{ "qe": {nil, map[string]*bintree{ "testdata": {nil, map[string]*bintree{ "olm": {nil, map[string]*bintree{ - "basic-bd-plain-image.yaml": {testQeTestdataOlmBasicBdPlainImageYaml, map[string]*bintree{}}, - "basic-bd-registry-image.yaml": {testQeTestdataOlmBasicBdRegistryImageYaml, map[string]*bintree{}}, - "binding-prefligth.yaml": {testQeTestdataOlmBindingPrefligthYaml, map[string]*bintree{}}, - "binding-prefligth_multirole.yaml": {testQeTestdataOlmBindingPrefligth_multiroleYaml, map[string]*bintree{}}, - "cip.yaml": {testQeTestdataOlmCipYaml, map[string]*bintree{}}, - "clustercatalog-secret-withlabel.yaml": {testQeTestdataOlmClustercatalogSecretWithlabelYaml, map[string]*bintree{}}, - "clustercatalog-secret.yaml": {testQeTestdataOlmClustercatalogSecretYaml, map[string]*bintree{}}, - "clustercatalog-withlabel.yaml": {testQeTestdataOlmClustercatalogWithlabelYaml, map[string]*bintree{}}, - "clustercatalog.yaml": {testQeTestdataOlmClustercatalogYaml, map[string]*bintree{}}, + "basic-bd-plain-image.yaml": {testQeTestdataOlmBasicBdPlainImageYaml, map[string]*bintree{}}, + "basic-bd-registry-image.yaml": {testQeTestdataOlmBasicBdRegistryImageYaml, map[string]*bintree{}}, + "binding-prefligth.yaml": {testQeTestdataOlmBindingPrefligthYaml, map[string]*bintree{}}, + "binding-prefligth_multirole.yaml": {testQeTestdataOlmBindingPrefligth_multiroleYaml, map[string]*bintree{}}, + "cip.yaml": {testQeTestdataOlmCipYaml, map[string]*bintree{}}, + "clustercatalog-secret-withlabel.yaml": {testQeTestdataOlmClustercatalogSecretWithlabelYaml, map[string]*bintree{}}, + "clustercatalog-secret.yaml": {testQeTestdataOlmClustercatalogSecretYaml, map[string]*bintree{}}, + "clustercatalog-with-pollinterval.yaml": {testQeTestdataOlmClustercatalogWithPollintervalYaml, map[string]*bintree{}}, + "clustercatalog-withlabel.yaml": {testQeTestdataOlmClustercatalogWithlabelYaml, map[string]*bintree{}}, + "clustercatalog.yaml": {testQeTestdataOlmClustercatalogYaml, map[string]*bintree{}}, "clusterextension-withselectorExpressions-WithoutChannelVersion.yaml": {testQeTestdataOlmClusterextensionWithselectorexpressionsWithoutchannelversionYaml, map[string]*bintree{}}, "clusterextension-withselectorLableExpressions-WithoutChannelVersion.yaml": {testQeTestdataOlmClusterextensionWithselectorlableexpressionsWithoutchannelversionYaml, map[string]*bintree{}}, "clusterextension-withselectorlabel-OwnSingle.yaml": {testQeTestdataOlmClusterextensionWithselectorlabelOwnsingleYaml, map[string]*bintree{}}, diff --git a/openshift/tests-extension/test/qe/specs/olmv1_cc.go b/openshift/tests-extension/test/qe/specs/olmv1_cc.go index 134fafd0c..76c8b3dcc 100644 --- a/openshift/tests-extension/test/qe/specs/olmv1_cc.go +++ b/openshift/tests-extension/test/qe/specs/olmv1_cc.go @@ -2,10 +2,17 @@ package specs import ( "context" + "crypto/tls" + "encoding/json" "fmt" + "io" + "net" + "net/http" + "os" "os/exec" "path/filepath" "regexp" + "strconv" "strings" "time" @@ -324,6 +331,267 @@ var _ = g.Describe("[sig-olmv1][Jira:OLM] clustercatalog", g.Label("NonHyperShif exutil.AssertWaitPollNoErr(errWait, "Cannot get the result") }) + g.It("PolarionID:85889-[OTP][Skipped:Disconnected]Validate catalogd content via port-forward [Serial]", func() { + caseID := "85889" + catalogName := "catalog-" + caseID + catalogImage := "quay.io/openshifttest/nginxolm-operator-index:nginxolm74924" + baseDir := exutil.FixturePath("testdata", "olm") + catalogTemplate := filepath.Join(baseDir, "clustercatalog-with-pollinterval.yaml") + outputDir := e2e.TestContext.OutputDir + if outputDir == "" { + outputDir = os.TempDir() + } + err := os.MkdirAll(outputDir, 0755) + o.Expect(err).NotTo(o.HaveOccurred()) + + redhatOperatorsFile := filepath.Join(outputDir, "redhat-operators-all.json") + customCatalogFile := filepath.Join(outputDir, "all.json") + + type catalogEntry struct { + Schema string `json:"schema"` + Name string `json:"name,omitempty"` + Package string `json:"package,omitempty"` + } + + parseCatalogEntries := func(data []byte) ([]catalogEntry, error) { + var entries []catalogEntry + for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + line = strings.TrimSpace(line) + if line == "" || !strings.HasPrefix(line, "{") { + continue + } + var entry catalogEntry + if err := json.Unmarshal([]byte(line), &entry); err != nil { + return nil, err + } + if entry.Schema != "" { + entries = append(entries, entry) + } + } + if len(entries) == 0 { + return nil, fmt.Errorf("no catalog entries parsed") + } + return entries, nil + } + + fetchToFile := func(url, path string) ([]byte, error) { + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec // uses local port-forward with self-signed cert + }, + Timeout: 2 * time.Minute, + } + req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status %s for %s", resp.Status, url) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if err = os.WriteFile(path, body, 0600); err != nil { + return nil, err + } + return body, nil + } + + getConsoleLogs := func() (string, error) { + return oc.AsAdmin().WithoutNamespace().Run("logs").Args("-n", "openshift-console", "deploy/console", "--since=10m").Output() + } + + getReconcileTotal := func() (float64, error) { + metrics, err := oc.AsAdmin().WithoutNamespace().Run("rsh").Args("-n", "openshift-console", "deploy/console", "curl", "-s", "localhost:9440/metrics").Output() + if err != nil { + return 0, err + } + re := regexp.MustCompile(`(?im)^.*reconcile_total.*clustercatalog.*\\s+([0-9]+(?:\\.[0-9]+)?)$`) + matches := re.FindAllStringSubmatch(metrics, -1) + if len(matches) == 0 { + return 0, fmt.Errorf("clustercatalog reconcile_total not found in metrics output") + } + var max float64 + for _, match := range matches { + value, err := strconv.ParseFloat(match[1], 64) + if err != nil { + continue + } + if value > max { + max = value + } + } + return max, nil + } + + clustercatalog := olmv1util.ClusterCatalogDescription{Name: catalogName} + deleted := false + defer func() { + if !deleted { + clustercatalog.Delete(oc) + } + }() + + g.By("Check OLM namespaces in ClusterOperator") + nsOutput, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("co", "olm", `-o=jsonpath={.status.relatedObjects[?(@.resource=="namespaces")].name}`).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(nsOutput).To(o.ContainSubstring("openshift-catalogd")) + o.Expect(nsOutput).To(o.ContainSubstring("openshift-operator-controller")) + + g.By("Verify OLM CRDs referenced") + crdOutput, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("co", "olm", `-o=jsonpath={.status.relatedObjects[?(@.resource=="customresourcedefinitions")].name}`).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(crdOutput).To(o.ContainSubstring("clustercatalogs.catalogd.operatorframework.io")) + o.Expect(crdOutput).To(o.ContainSubstring("clusterextensions.olm.operatorframework.io")) + + g.By("Create test ClusterCatalog") + clustercatalog.Template = catalogTemplate + clustercatalog.Imageref = catalogImage + clustercatalog.PollIntervalMinutes = "300s" + clustercatalog.Create(oc) + + g.By("Verify ClusterCatalog list and phases") + listOutput, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("clustercatalog", `-o=jsonpath={range .items[*]}{.metadata.name}{"="}{.status.phase}{"\n"}{end}`).Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(listOutput).To(o.ContainSubstring("openshift-certified-operators=")) + o.Expect(listOutput).To(o.ContainSubstring("openshift-community-operators=")) + o.Expect(listOutput).To(o.ContainSubstring("openshift-redhat-operators=")) + o.Expect(listOutput).To(o.ContainSubstring("openshift-redhat-marketplace=")) + var catalogPhase string + for _, line := range strings.Split(strings.TrimSpace(listOutput), "\n") { + if strings.HasPrefix(line, catalogName+"=") { + catalogPhase = strings.TrimPrefix(line, catalogName+"=") + break + } + } + o.Expect(catalogPhase).NotTo(o.BeEmpty()) + + g.By("Find catalogd service") + svcPorts, err := oc.AsAdmin().WithoutNamespace().Run("get").Args("svc", "-n", "openshift-catalogd", "catalogd-catalogserver", "-o=jsonpath={.spec.ports[*].port}").Output() + o.Expect(err).NotTo(o.HaveOccurred()) + o.Expect(svcPorts).To(o.ContainSubstring("443")) + + g.By("Port-forward catalogd service") + pfCmd, _, _, err := oc.AsAdmin().WithoutNamespace().Run("port-forward").Args("-n", "openshift-catalogd", "svc/catalogd-catalogserver", "8443:443").Background() + o.Expect(err).NotTo(o.HaveOccurred()) + defer func() { + _ = pfCmd.Process.Kill() + _ = pfCmd.Wait() + }() + errWait := wait.PollUntilContextTimeout(context.TODO(), 2*time.Second, 30*time.Second, false, func(ctx context.Context) (bool, error) { + conn, err := net.DialTimeout("tcp", "127.0.0.1:8443", 2*time.Second) + if err != nil { + return false, nil + } + _ = conn.Close() + return true, nil + }) + exutil.AssertWaitPollNoErr(errWait, "catalogd port-forward did not become ready") + + g.By("Create redhat-operators-all.json") + redhatData, err := fetchToFile("https://localhost:8443/catalogs/redhat-operators/all.json", redhatOperatorsFile) + o.Expect(err).NotTo(o.HaveOccurred()) + _, err = os.Stat(redhatOperatorsFile) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Create all.json for catalog-85889") + customData, err := fetchToFile(fmt.Sprintf("https://localhost:8443/catalogs/%s/all.json", catalogName), customCatalogFile) + o.Expect(err).NotTo(o.HaveOccurred()) + _, err = os.Stat(customCatalogFile) + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("List OLM packages from redhat-operators") + redhatEntries, err := parseCatalogEntries(redhatData) + o.Expect(err).NotTo(o.HaveOccurred()) + var packageNames []string + for _, entry := range redhatEntries { + if entry.Schema == "olm.package" && entry.Name != "" { + packageNames = append(packageNames, entry.Name) + } + } + o.Expect(packageNames).NotTo(o.BeEmpty()) + + g.By("Get channel info for nginx68821") + customEntries, err := parseCatalogEntries(customData) + o.Expect(err).NotTo(o.HaveOccurred()) + channelFound := false + for _, entry := range customEntries { + if entry.Schema == "olm.channel" && entry.Package == "nginx68821" { + channelFound = true + break + } + } + o.Expect(channelFound).To(o.BeTrue()) + + g.By("Get bundle info for nginx68821.v1.1.0") + bundleFound := false + for _, entry := range customEntries { + if entry.Schema == "olm.bundle" && entry.Package == "nginx68821" && entry.Name == "nginx68821.v1.1.0" { + bundleFound = true + break + } + } + o.Expect(bundleFound).To(o.BeTrue()) + + g.By("Check ClusterCatalog reconciliation logs") + consoleLogs, err := getConsoleLogs() + o.Expect(err).NotTo(o.HaveOccurred()) + consoleLogsLower := strings.ToLower(consoleLogs) + o.Expect(consoleLogsLower).To(o.ContainSubstring("reconcil")) + o.Expect(consoleLogsLower).To(o.ContainSubstring("clustercatalog")) + o.Expect(consoleLogs).To(o.ContainSubstring(catalogName)) + o.Expect(consoleLogsLower).NotTo(o.ContainSubstring("panic")) + o.Expect(consoleLogsLower).NotTo(o.ContainSubstring("stack trace")) + + g.By("Check reconcile_total metrics before patch") + reconcileBefore, err := getReconcileTotal() + o.Expect(err).NotTo(o.HaveOccurred()) + + g.By("Patch catalog-85889 image") + patch := `{"spec":{"source":{"type":"Image","image":{"ref":"quay.io/operatorhubio/catalog:latest"}}}}` + clustercatalog.Patch(oc, patch) + + g.By("Confirm reconcile_total increases after patch") + errWait = wait.PollUntilContextTimeout(context.TODO(), 10*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { + reconcileAfter, err := getReconcileTotal() + if err != nil { + return false, nil + } + return reconcileAfter > reconcileBefore, nil + }) + exutil.AssertWaitPollNoErr(errWait, "reconcile_total did not increase after patch") + + g.By("Delete test ClusterCatalog") + clustercatalog.Delete(oc) + deleted = true + + g.By("Verify clean finalization logs") + errWait = wait.PollUntilContextTimeout(context.TODO(), 10*time.Second, 2*time.Minute, false, func(ctx context.Context) (bool, error) { + consoleLogs, err = getConsoleLogs() + if err != nil { + return false, nil + } + consoleLogsLower = strings.ToLower(consoleLogs) + if !strings.Contains(consoleLogsLower, "finaliz") { + return false, nil + } + if !strings.Contains(consoleLogs, catalogName) { + return false, nil + } + if strings.Contains(consoleLogsLower, "panic") || strings.Contains(consoleLogsLower, "stack trace") { + return false, fmt.Errorf("panic or stack trace detected in logs") + } + return true, nil + }) + exutil.AssertWaitPollNoErr(errWait, "catalog-85889 finalization logs not found or contained errors") + }) + g.It("PolarionID:73219-[OTP][Skipped:Disconnected]Fetch deprecation data from the catalogd http server", func() { var ( baseDir = exutil.FixturePath("testdata", "olm")