Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion hack/govulncheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ set -o errexit
# - https://pkg.go.dev/vuln/GO-2026-5037 - high-CPU VerifyHostname behavior via repeated hostname splitting.
# - https://pkg.go.dev/vuln/GO-2026-5038 - Decoding a maliciously-crafted MIME header containing many invalid encoded-words can consume excessive CPU.
# - https://pkg.go.dev/vuln/GO-2026-5039 - net/textproto package includes its input as part of the error.
KNOWN_VULNS_PATTERN="GO-2025-3521|GO-2025-3547|GO-2026-4918|GO-2026-4971|GO-2026-5026|GO-2026-5037|GO-2026-5038|GO-2026-5039"
# - https://pkg.go.dev/vuln/GO-2026-5856 - crypto/tls: Encrypted Client Hello handshakes can be de-anonymized via PSK identity disclosure
KNOWN_VULNS_PATTERN="GO-2025-3521|GO-2025-3547|GO-2026-4918|GO-2026-4971|GO-2026-5026|GO-2026-5037|GO-2026-5038|GO-2026-5039|GO-2026-5856"

GOVULNCHECK_BIN="${1:-}"
OUTPUT_DIR="${2:-}"
Expand Down
31 changes: 30 additions & 1 deletion test/e2e/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,27 @@ import (
appsv1 "k8s.io/api/apps/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"

operatorv1alpha1 "github.com/openshift/external-secrets-operator/api/v1alpha1"
"github.com/openshift/external-secrets-operator/pkg/controller/common"
externalsecrets "github.com/openshift/external-secrets-operator/pkg/controller/external_secrets"
"github.com/openshift/external-secrets-operator/test/utils"
)

// ensureExternalSecretsConfigReady creates the cluster ExternalSecretsConfig CR when missing
// and waits until Ready=True. Shared by suite Describes that may run before e2e_test BeforeAll.
func ensureExternalSecretsConfigReady(ctx context.Context) error {
if suiteRuntimeClient == nil || suiteDynamicClient == nil {
if suiteRuntimeClient == nil || suiteDynamicClient == nil || suiteClientset == nil {
return fmt.Errorf("suite clients not initialized")
}

if err := waitForNamespaceTermination(ctx, suiteClientset, externalsecrets.OperandDefaultNamespace, 2*time.Minute); err != nil {
return fmt.Errorf("waiting for operand namespace to finish terminating: %w", err)
}

esc := &operatorv1alpha1.ExternalSecretsConfig{}
err := suiteRuntimeClient.Get(ctx, client.ObjectKey{Name: common.ExternalSecretsConfigObjectName}, esc)
if err != nil {
Expand All @@ -58,6 +64,29 @@ func ensureExternalSecretsConfigReady(ctx context.Context) error {
return utils.WaitForExternalSecretsConfigReady(ctx, suiteDynamicClient, common.ExternalSecretsConfigObjectName, 2*time.Minute)
}

func waitForNamespaceTermination(ctx context.Context, clientset *kubernetes.Clientset, namespace string, timeout time.Duration) error {
ns, err := clientset.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
if ns.Status.Phase != "Terminating" {
return nil
Comment on lines +68 to +76

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '1,140p' test/e2e/helpers_test.go
printf '\n--- usages ---\n'
rg -n "Terminating|DeletionTimestamp|is.*terminating|namespace.*terminating|helpers_test.go" test/e2e -g '*.go'

Repository: openshift/external-secrets-operator

Length of output: 5754


🏁 Script executed:

sed -n '2050,2110p' test/e2e/e2e_test.go
python3 - <<'PY'
from pathlib import Path
text = Path('test/e2e/helpers_test.go').read_text()
start = text.index('func waitForNamespaceTermination')
print(text[start:text.index('// resourceType defines', start)])
PY

Repository: openshift/external-secrets-operator

Length of output: 3179


🏁 Script executed:

rg -n "ensureExternalSecretsConfigReady|waitForNamespaceTermination|OperandDefaultNamespace" test/e2e -g '*.go'
sed -n '1,90p' test/e2e/helpers_test.go

Repository: openshift/external-secrets-operator

Length of output: 4188


🌐 Web query:

Kubernetes NamespaceStatus phase Terminating deletionTimestamp before phase changes namespace deletion semantics

💡 Result:

In Kubernetes, the interaction between the Namespace.Status.Phase and the metadata.deletionTimestamp is a core mechanism of the graceful deletion process [1][2]. When a DELETE request is issued for a namespace, the API server sets the metadata.deletionTimestamp to the current server time [1][3]. This timestamp acts as the primary indicator that a graceful deletion has been requested [3][4]. Upon observing this change, the namespace-controller updates the Namespace.Status.Phase from Active to Terminating [1][5]. The deletion process follows these semantics: 1. Request and Marking: The presence of a non-nil deletionTimestamp prevents the creation of new objects within the namespace [6]. The phase transition to Terminating signals to users and system components that the namespace is undergoing cleanup and is no longer available for new workloads [7][8]. 2. Resource Cleanup: The namespace-controller begins iterating through and deleting all content (resources) associated with the namespace [1][6]. 3. Finalization: The namespace itself contains a spec.finalizers list (typically including the kubernetes finalizer) [1][2]. The namespace cannot be removed from storage until this list is empty [3][4]. The namespace-controller is responsible for purging the namespace's contents and, upon successful cleanup, removing the finalizers [1][6]. 4. Permanent Removal: Once all finalizers are removed and the deletionTimestamp is set, the namespace-controller sends a final DELETE action to the API server to permanently remove the namespace object from etcd storage [1][2]. If a namespace remains stuck in the Terminating phase, it is typically because one or more finalizers have not been removed, often due to a controller failing to successfully clean up the associated resources or a manual error (e.g., specifying a non-existent finalizer) [9][3][8]. Because the deletionTimestamp is already set, the namespace is effectively "locked" into the deletion flow until the finalizers are cleared [3][4]. Top results: [1][5][6][9][3]

Citations:


Treat a non-nil deletionTimestamp as terminating. Lines 75-76 can return while the namespace is already marked for deletion but still reports Active, letting setup continue before teardown finishes and making later readiness checks flaky.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/helpers_test.go` around lines 68 - 76, Update the namespace
termination check in the helper around the namespace Get call to treat any
non-nil ns.DeletionTimestamp as terminating, regardless of ns.Status.Phase.
Preserve the existing nil return for namespaces that are neither marked for
deletion nor in the Terminating phase.

}
return wait.PollUntilContextTimeout(ctx, 5*time.Second, timeout, true, func(ctx context.Context) (bool, error) {
_, err := clientset.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return true, nil
}
if err != nil {
return false, err
}
return false, nil
})
}

// resourceType defines a Kubernetes resource type to verify annotations on
type resourceType struct {
name string
Expand Down
Loading