@@ -67,6 +67,95 @@ timeout_seconds() {
6767 echo " $val "
6868}
6969
70+ # Discover CR type plural names for CRDs whose API group ends with the given suffix.
71+ # Arguments: api_group_suffix (e.g. "strimzi.io"), all_crds (pre-fetched CRD data)
72+ # Output: space-separated list of plural CR type names, or empty string
73+ filter_cr_types () {
74+ local suffix=" $1 "
75+ local all_crds=" $2 "
76+ echo " $all_crds " \
77+ | awk -v suf=" $suffix " ' index($1, suf) == length($1) - length(suf) + 1 { printf "%s ", $2 }' \
78+ | sed ' s/ $//'
79+ }
80+
81+ # Check if /dev/tty is available for interactive prompts.
82+ # Needed for `curl | bash` where stdin is the pipe.
83+ is_interactive () {
84+ [ -c /dev/tty ] 2> /dev/null
85+ }
86+
87+ # Prompt user with a yes/no question via /dev/tty. Defaults to No.
88+ # Arguments: prompt message
89+ # Returns 0 for yes, 1 for no
90+ prompt_yes_no () {
91+ local prompt=" $1 "
92+ local answer
93+ echo -en " ${YELLOW}${prompt} [y/N]: ${NC} " > /dev/tty
94+ read -r answer < /dev/tty
95+ case " $answer " in
96+ [yY]|[yY][eE][sS]) return 0 ;;
97+ * ) return 1 ;;
98+ esac
99+ }
100+
101+ # List unlabeled CRs for given CR types across all namespaces.
102+ # Arguments: cr_types (space-separated list)
103+ # Returns 0 if any unlabeled CRs found, 1 if none
104+ list_unlabeled_crs () {
105+ local cr_types=" $1 "
106+ local found=false
107+ for cr_type in $cr_types ; do
108+ local count
109+ count=$( kubectl get " $cr_type " -A --selector=' !app.kubernetes.io/part-of' --no-headers 2> /dev/null | wc -l | tr -d ' ' ) || count=0
110+ if [ " $count " -gt 0 ]; then
111+ found=true
112+ kubectl get " $cr_type " -A --selector=' !app.kubernetes.io/part-of' 2> /dev/null || true
113+ fi
114+ done
115+ [ " $found " = true ]
116+ }
117+
118+ # Delete unlabeled CRs for given CR types across all namespaces.
119+ # Arguments: cr_types (space-separated list)
120+ delete_unlabeled_crs () {
121+ local cr_types=" $1 "
122+ for cr_type in $cr_types ; do
123+ local count
124+ count=$( kubectl get " $cr_type " -A --selector=' !app.kubernetes.io/part-of' --no-headers 2> /dev/null | wc -l | tr -d ' ' ) || count=0
125+ if [ " $count " -gt 0 ]; then
126+ kubectl delete " $cr_type " -A --selector=' !app.kubernetes.io/part-of' --ignore-not-found=true --wait=false 2> /dev/null || true
127+ fi
128+ done
129+ }
130+
131+ # Wait for unlabeled CRs of given types to be fully removed.
132+ # Arguments: cr_types (space-separated list)
133+ # Returns 0 if all removed, 1 if timeout
134+ wait_for_unlabeled_cr_removal () {
135+ local cr_types=" $1 "
136+ local max_wait
137+ max_wait=$( timeout_seconds)
138+ local elapsed=0
139+
140+ while [ $elapsed -lt " $max_wait " ]; do
141+ local total=0
142+ for cr_type in $cr_types ; do
143+ local count
144+ count=$( kubectl get " $cr_type " -A --selector=' !app.kubernetes.io/part-of' --no-headers 2> /dev/null | wc -l | tr -d ' ' )
145+ total=$(( total + count))
146+ done
147+ if [ " $total " -eq 0 ]; then
148+ return 0
149+ fi
150+ if [ " ${interrupted:- false} " = true ]; then
151+ return 1
152+ fi
153+ sleep 5
154+ elapsed=$(( elapsed + 5 ))
155+ done
156+ return 1
157+ }
158+
70159# Wait for all custom resources of a given type to be fully removed
71160# Returns 0 if all removed, 1 if timeout
72161wait_for_cr_removal () {
@@ -82,6 +171,9 @@ wait_for_cr_removal() {
82171 if [ " $count " -eq 0 ]; then
83172 return 0
84173 fi
174+ if [ " ${interrupted:- false} " = true ]; then
175+ return 1
176+ fi
85177 sleep 5
86178 elapsed=$(( elapsed + 5 ))
87179 done
@@ -111,14 +203,10 @@ has_unlabeled_crs() {
111203 return 1
112204}
113205
114- # Strimzi CR types to check for shared usage
115- STRIMZI_CR_TYPES=" kafkas kafkanodepools kafkatopics kafkausers kafkaconnects kafkamirrormakers kafkamirrormaker2s kafkabridges kafkaconnectors kafkarebalances"
116-
117- # Apicurio Registry CR types
118- APICURIO_CR_TYPES=" apicurioregistry3s"
119-
120- # StreamsHub Console CR types
121- CONSOLE_CR_TYPES=" consoles"
206+ # API group suffixes used to discover CR types dynamically from installed CRDs
207+ STRIMZI_API_GROUP_SUFFIX=" strimzi.io"
208+ APICURIO_API_GROUP_SUFFIX=" apicur.io"
209+ CONSOLE_API_GROUP_SUFFIX=" streamshub.github.com"
122210
123211main () {
124212 echo " "
@@ -130,17 +218,134 @@ main() {
130218 fi
131219 echo " "
132220
221+ # Discover CR types dynamically from installed CRDs
222+ local all_crds
223+ all_crds=$( kubectl get crds \
224+ -o jsonpath=' {range .items[*]}{.spec.group}{" "}{.spec.names.plural}{"\n"}{end}' \
225+ 2> /dev/null || true)
226+
227+ STRIMZI_CR_TYPES=$( filter_cr_types " $STRIMZI_API_GROUP_SUFFIX " " $all_crds " )
228+ APICURIO_CR_TYPES=$( filter_cr_types " $APICURIO_API_GROUP_SUFFIX " " $all_crds " )
229+ CONSOLE_CR_TYPES=$( filter_cr_types " $CONSOLE_API_GROUP_SUFFIX " " $all_crds " )
230+
231+ info " Discovered CR types:"
232+ info " Strimzi: ${STRIMZI_CR_TYPES:- <none>} "
233+ info " Apicurio Registry: ${APICURIO_CR_TYPES:- <none>} "
234+ info " StreamsHub Console: ${CONSOLE_CR_TYPES:- <none>} "
235+ echo " "
236+
237+ # Catch Ctrl+C so the shell doesn't silently exit before reaching interactive prompts
238+ local interrupted=false
239+ trap ' warn "Caught interrupt, continuing cleanup..."; interrupted=true' INT
240+
133241 # Track which operator groups have shared CRDs
134242 local strimzi_shared=false
135243 local apicurio_shared=false
136244 local console_shared=false
137245
138- # --- Phase 1: Delete operands (stack layer) ---
246+ # --- Phase 1: Interactive cleanup of user-created CRs ---
247+ # Must run BEFORE stack deletion so operators are still alive to process finalizers
248+ info " Phase 1: Checking for user-created custom resources..."
249+
250+ local has_user_crs=false
251+
252+ # Check each operator group for unlabeled CRs
253+ local strimzi_user_crs=false
254+ local apicurio_user_crs=false
255+ local console_user_crs=false
256+
257+ if list_unlabeled_crs " $STRIMZI_CR_TYPES " > /dev/null 2>&1 ; then
258+ strimzi_user_crs=true
259+ has_user_crs=true
260+ fi
261+ if list_unlabeled_crs " $APICURIO_CR_TYPES " > /dev/null 2>&1 ; then
262+ apicurio_user_crs=true
263+ has_user_crs=true
264+ fi
265+ if list_unlabeled_crs " $CONSOLE_CR_TYPES " > /dev/null 2>&1 ; then
266+ console_user_crs=true
267+ has_user_crs=true
268+ fi
269+
270+ if [ " $has_user_crs " = true ]; then
271+ if is_interactive; then
272+ warn " Found user-created custom resources (not part of the quick-start)."
273+ warn " These resources will cause operator CRDs to be retained if not deleted."
274+ echo " "
275+
276+ if [ " $strimzi_user_crs " = true ]; then
277+ info " Strimzi resources without quick-start label:"
278+ list_unlabeled_crs " $STRIMZI_CR_TYPES " 2> /dev/null | sed ' s/^/ /'
279+ echo " "
280+ if prompt_yes_no " Delete these Strimzi resources?" ; then
281+ info " Deleting unlabeled Strimzi resources..."
282+ delete_unlabeled_crs " $STRIMZI_CR_TYPES "
283+ if wait_for_unlabeled_cr_removal " $STRIMZI_CR_TYPES " ; then
284+ info " Strimzi resources removed"
285+ strimzi_user_crs=false
286+ else
287+ warn " Some Strimzi resources were not fully removed within timeout"
288+ fi
289+ else
290+ info " Skipping Strimzi resource deletion"
291+ fi
292+ echo " "
293+ fi
294+
295+ if [ " $apicurio_user_crs " = true ]; then
296+ info " Apicurio Registry resources without quick-start label:"
297+ list_unlabeled_crs " $APICURIO_CR_TYPES " 2> /dev/null | sed ' s/^/ /'
298+ echo " "
299+ if prompt_yes_no " Delete these Apicurio Registry resources?" ; then
300+ info " Deleting unlabeled Apicurio Registry resources..."
301+ delete_unlabeled_crs " $APICURIO_CR_TYPES "
302+ if wait_for_unlabeled_cr_removal " $APICURIO_CR_TYPES " ; then
303+ info " Apicurio Registry resources removed"
304+ apicurio_user_crs=false
305+ else
306+ warn " Some Apicurio Registry resources were not fully removed within timeout"
307+ fi
308+ else
309+ info " Skipping Apicurio Registry resource deletion"
310+ fi
311+ echo " "
312+ fi
313+
314+ if [ " $console_user_crs " = true ]; then
315+ info " StreamsHub Console resources without quick-start label:"
316+ list_unlabeled_crs " $CONSOLE_CR_TYPES " 2> /dev/null | sed ' s/^/ /'
317+ echo " "
318+ if prompt_yes_no " Delete these StreamsHub Console resources?" ; then
319+ info " Deleting unlabeled Console resources..."
320+ delete_unlabeled_crs " $CONSOLE_CR_TYPES "
321+ if wait_for_unlabeled_cr_removal " $CONSOLE_CR_TYPES " ; then
322+ info " Console resources removed"
323+ console_user_crs=false
324+ else
325+ warn " Some Console resources were not fully removed within timeout"
326+ fi
327+ else
328+ info " Skipping Console resource deletion"
329+ fi
330+ echo " "
331+ fi
332+ else
333+ warn " Non-interactive mode: skipping user-created resource cleanup."
334+ warn " User-created CRs were detected but cannot prompt for deletion."
335+ warn " Phase 3 will treat these as shared resources and retain their CRDs."
336+ echo " "
337+ fi
338+ else
339+ info " No user-created custom resources found"
340+ fi
341+ echo " "
342+
343+ # --- Phase 2: Delete operands (stack layer) ---
139344 local stack_url
140345 stack_url=$( kustomize_url " stack" )
141- info " Phase 1 : Deleting operands..."
346+ info " Phase 2 : Deleting operands..."
142347 info " Deleting: ${stack_url} "
143- kubectl delete -k " ${stack_url} " --ignore-not-found=true 2> /dev/null || true
348+ kubectl delete -k " ${stack_url} " --ignore-not-found=true --wait=false 2> /dev/null || true
144349 echo " "
145350
146351 # --- Wait for CRs to be fully removed ---
@@ -184,10 +389,11 @@ main() {
184389 if [ " $removal_failed " = true ]; then
185390 warn " Some custom resources were not fully removed. Proceeding with caution..."
186391 fi
392+ interrupted=false
187393 echo " "
188394
189- # --- Phase 2 : Shared-cluster safety checks ---
190- info " Phase 2 : Checking for shared CRD usage..."
395+ # --- Phase 3 : Shared-cluster safety checks ---
396+ info " Phase 3 : Checking for shared CRD usage..."
191397
192398 if has_unlabeled_crs " $STRIMZI_CR_TYPES " ; then
193399 strimzi_shared=true
@@ -211,17 +417,17 @@ main() {
211417 fi
212418 echo " "
213419
214- # --- Phase 3 : Delete operators and CRDs ---
420+ # --- Phase 4 : Delete operators and CRDs ---
215421 if [ " $strimzi_shared " = false ] && [ " $apicurio_shared " = false ] && [ " $console_shared " = false ]; then
216422 # No shared CRDs — safe to delete the entire base layer
217423 local base_url
218424 base_url=$( kustomize_url " base" )
219- info " Phase 3 : Deleting operators and CRDs (no shared usage detected)..."
425+ info " Phase 4 : Deleting operators and CRDs (no shared usage detected)..."
220426 info " Deleting: ${base_url} "
221427 kubectl delete -k " ${base_url} " --ignore-not-found=true 2> /dev/null || true
222428 else
223429 # Some operator groups have shared CRDs — selective cleanup
224- info " Phase 3 : Selective operator removal (some CRDs are shared)..."
430+ info " Phase 4 : Selective operator removal (some CRDs are shared)..."
225431
226432 if [ " $strimzi_shared " = false ]; then
227433 info " Removing Strimzi operator (full removal including CRDs)..."
0 commit comments