Skip to content

Commit f082d69

Browse files
pedjakclaude
andcommitted
fix: make demo e2e catalog queries resilient to transient failures
The generate-demos CI job fails ~45% of the time with jq exit status 5 on the ClusterCatalog Quickstart scenario. Three issues contribute: - jq -s (slurp mode) buffers the entire operatorhubio FBC response in memory before processing, risking system errors on large catalogs - catalog content queries run exactly once with no retry, so any transient port-forward or network hiccup fails the step immediately - bash() does not attach stderr to ExitError, making failures opaque Remove jq slurp mode so each JSON object is processed in constant memory, prefixing filters with 'objects' to skip non-object values in the FBC stream. Wrap CatalogContainsSomePackages, PackageHasSomeChannels, and PackageHasSomeBundles in waitFor for retry on transient errors. Add curl --retry for transport-level resilience, --compressed to handle gzip-encoded responses, and --fail to prevent HTTP error bodies from reaching jq. Re-establish dead port-forwards automatically via liveness checks. Inject stderr into ExitError in bash() to match k8sClient diagnostics. Log catalog query errors at V(0) so CI timeout failures are diagnosable. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 7ec6a0e commit f082d69

1 file changed

Lines changed: 71 additions & 39 deletions

File tree

‎test/e2e/steps/demo_steps.go‎

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"crypto/tls"
77
"encoding/json"
8+
"errors"
89
"fmt"
910
"net"
1011
"net/http"
@@ -41,6 +42,10 @@ func bash(ctx context.Context, script string) (string, error) {
4142

4243
if err != nil {
4344
logger.V(1).Info("Failed to run", "command", script, "stderr", stderr, "error", err)
45+
var exitErr *exec.ExitError
46+
if errors.As(err, &exitErr) {
47+
exitErr.Stderr = stderrBuf.Bytes()
48+
}
4449
}
4550
logger.V(1).Info("Output", "command", script, "output", stdout)
4651

@@ -81,7 +86,15 @@ func CatalogReportsConditionWithoutReason(ctx context.Context, catalogUserName,
8186
func ensureCatalogPortForward(ctx context.Context) (string, error) {
8287
sc := scenarioCtx(ctx)
8388
if sc.catalogAddr != "" {
84-
return sc.catalogAddr, nil
89+
if catalogPortForwardAlive(sc.catalogAddr) {
90+
return sc.catalogAddr, nil
91+
}
92+
logger.V(1).Info("Catalog port-forward is dead, re-establishing", "addr", sc.catalogAddr)
93+
if sc.catalogCleanup != nil {
94+
sc.catalogCleanup()
95+
}
96+
sc.catalogAddr = ""
97+
sc.catalogCleanup = nil
8598
}
8699

87100
addr, cleanup, err := portForward(ctx, componentNamespaces["catalogd"], "service/catalogd-service", 443)
@@ -92,68 +105,87 @@ func ensureCatalogPortForward(ctx context.Context) (string, error) {
92105
sc.catalogCleanup = cleanup
93106

94107
waitFor(ctx, func() bool {
95-
client := &http.Client{
96-
Timeout: 3 * time.Second,
97-
Transport: &http.Transport{
98-
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
99-
DialContext: (&net.Dialer{Timeout: 2 * time.Second}).DialContext,
100-
},
101-
}
102-
resp, err := client.Get(fmt.Sprintf("https://%s/", addr))
103-
if err != nil {
104-
return false
105-
}
106-
resp.Body.Close()
107-
return true
108+
return catalogPortForwardAlive(addr)
108109
})
109110
return addr, nil
110111
}
111112

113+
func catalogPortForwardAlive(addr string) bool {
114+
client := &http.Client{
115+
Timeout: 3 * time.Second,
116+
Transport: &http.Transport{
117+
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
118+
DialContext: (&net.Dialer{Timeout: 2 * time.Second}).DialContext,
119+
},
120+
}
121+
resp, err := client.Get(fmt.Sprintf("https://%s/", addr))
122+
if err != nil {
123+
return false
124+
}
125+
resp.Body.Close()
126+
return true
127+
}
128+
112129
func catalogCurlJq(ctx context.Context, catalogName, jqFilter string) (string, error) {
113130
addr, err := ensureCatalogPortForward(ctx)
114131
if err != nil {
115132
return "", err
116133
}
117134
script := fmt.Sprintf(
118-
`curl -s -k https://%s/catalogs/%s/api/v1/all | jq -s '%s'`,
135+
`curl -sS -k --compressed --fail --retry 3 --retry-delay 2 --retry-all-errors https://%s/catalogs/%s/api/v1/all | jq '%s'`,
119136
addr, catalogName, jqFilter,
120137
)
121138
return bash(ctx, script)
122139
}
123140

124141
func CatalogContainsSomePackages(ctx context.Context, catalogName string) error {
125-
out, err := catalogCurlJq(ctx, catalogName,
126-
`.[] | select(.schema == "olm.package") | .name`)
127-
if err != nil {
128-
return err
129-
}
130-
if strings.TrimSpace(out) == "" {
131-
return fmt.Errorf("catalog %q contains no packages", catalogName)
132-
}
142+
waitFor(ctx, func() bool {
143+
out, err := catalogCurlJq(ctx, catalogName,
144+
`objects | select(.schema == "olm.package") | .name`)
145+
if err != nil {
146+
logger.Info("Catalog query failed, retrying", "catalog", catalogName, "error", err, "stderr", stderrOutput(err))
147+
return false
148+
}
149+
if strings.TrimSpace(out) == "" {
150+
logger.Info("Catalog returned no packages, retrying", "catalog", catalogName)
151+
return false
152+
}
153+
return true
154+
})
133155
return nil
134156
}
135157

136158
func PackageHasSomeChannels(ctx context.Context, packageName, catalogName string) error {
137-
out, err := catalogCurlJq(ctx, catalogName,
138-
fmt.Sprintf(`.[] | select(.schema == "olm.channel") | select(.package == "%s") | .name`, packageName))
139-
if err != nil {
140-
return err
141-
}
142-
if strings.TrimSpace(out) == "" {
143-
return fmt.Errorf("package %q in catalog %q has no channels", packageName, catalogName)
144-
}
159+
waitFor(ctx, func() bool {
160+
out, err := catalogCurlJq(ctx, catalogName,
161+
fmt.Sprintf(`objects | select(.schema == "olm.channel") | select(.package == "%s") | .name`, packageName))
162+
if err != nil {
163+
logger.Info("Catalog query failed, retrying", "catalog", catalogName, "package", packageName, "error", err, "stderr", stderrOutput(err))
164+
return false
165+
}
166+
if strings.TrimSpace(out) == "" {
167+
logger.Info("Package has no channels, retrying", "catalog", catalogName, "package", packageName)
168+
return false
169+
}
170+
return true
171+
})
145172
return nil
146173
}
147174

148175
func PackageHasSomeBundles(ctx context.Context, packageName, catalogName string) error {
149-
out, err := catalogCurlJq(ctx, catalogName,
150-
fmt.Sprintf(`.[] | select(.schema == "olm.bundle") | select(.package == "%s") | .name`, packageName))
151-
if err != nil {
152-
return err
153-
}
154-
if strings.TrimSpace(out) == "" {
155-
return fmt.Errorf("package %q in catalog %q has no bundles", packageName, catalogName)
156-
}
176+
waitFor(ctx, func() bool {
177+
out, err := catalogCurlJq(ctx, catalogName,
178+
fmt.Sprintf(`objects | select(.schema == "olm.bundle") | select(.package == "%s") | .name`, packageName))
179+
if err != nil {
180+
logger.Info("Catalog query failed, retrying", "catalog", catalogName, "package", packageName, "error", err, "stderr", stderrOutput(err))
181+
return false
182+
}
183+
if strings.TrimSpace(out) == "" {
184+
logger.Info("Package has no bundles, retrying", "catalog", catalogName, "package", packageName)
185+
return false
186+
}
187+
return true
188+
})
157189
return nil
158190
}
159191

0 commit comments

Comments
 (0)