@@ -3,6 +3,29 @@ import { execFile } from "child_process";
33import { Log } from "./logs" ;
44
55export class LogUtils {
6+ /**
7+ * Executes a command and returns the output as a promise.
8+ *
9+ * @param command The command to execute
10+ * @param args An array of arguments for the command
11+ * @returns A promise that resolves with the command output
12+ */
13+ static executeCommand ( command : string , args : string [ ] = [ ] ) : Promise < string > {
14+ return new Promise ( ( resolve , reject ) => {
15+ execFile ( command , args , { encoding : "utf8" } , ( error , stdout , stderr ) => {
16+ if ( error ) {
17+ console . error ( "Error executing command:" , error ) ;
18+ reject ( `Error: ${ error . message } ` ) ;
19+ return ;
20+ }
21+ if ( stderr ) {
22+ console . warn ( "stderr warning:" , stderr ) ;
23+ }
24+ resolve ( stdout ) ;
25+ } ) ;
26+ } ) ;
27+ }
28+
629 /**
730 * Validates if the actual log matches the expected log values.
831 * It compares both primitive and nested object properties.
@@ -11,7 +34,6 @@ export class LogUtils {
1134 * @param expected The expected log values to validate against
1235 */
1336 public static validateLog ( actual : Log , expected : Partial < Log > ) {
14- // Loop through each key in the expected log object
1537 Object . keys ( expected ) . forEach ( ( key ) => {
1638 const expectedValue = expected [ key as keyof Log ] ;
1739 const actualValue = actual [ key as keyof Log ] ;
@@ -42,71 +64,116 @@ export class LogUtils {
4264 }
4365
4466 /**
45- * Executes a command and returns the output as a promise .
67+ * Lists all pods in the specified namespace and returns their details .
4668 *
47- * @param command The command to execute
48- * @param args An array of arguments for the command
49- * @returns A promise that resolves with the command output
69+ * @param namespace The namespace to list pods from
70+ * @returns A promise that resolves with the pod details
5071 */
51- static executeCommand ( command : string , args : string [ ] = [ ] ) : Promise < string > {
52- return new Promise ( ( resolve , reject ) => {
53- execFile ( command , args , { encoding : "utf8" } , ( error , stdout , stderr ) => {
54- if ( error ) {
55- console . error ( "Error executing command:" , error ) ;
56- reject ( `Error: ${ error . message } ` ) ;
57- return ;
58- }
59- if ( stderr ) {
60- console . warn ( "stderr warning:" , stderr ) ;
61- }
62- resolve ( stdout ) ;
63- } ) ;
64- } ) ;
72+ static async listPods ( namespace : string ) : Promise < string > {
73+ const args = [ "get" , "pods" , "-n" , namespace , "-o" , "wide" ] ;
74+ try {
75+ console . log ( "Fetching pod list with command:" , "oc" , args . join ( " " ) ) ;
76+ return await LogUtils . executeCommand ( "oc" , args ) ;
77+ } catch ( error ) {
78+ console . error ( "Error listing pods:" , error ) ;
79+ throw new Error (
80+ `Failed to list pods in namespace "${ namespace } ": ${ error } ` ,
81+ ) ;
82+ }
6583 }
6684
6785 /**
68- * Fetches the logs from pods that match the fixed pod selector and applies a grep filter.
69- * The pod selector is:
70- * - app.kubernetes.io/component=backstage
71- * - app.kubernetes.io/instance=redhat-developer-hub
72- * - app.kubernetes.io/name=developer-hub
86+ * Fetches detailed information about a specific pod.
87+ *
88+ * @param podName The name of the pod to fetch details for
89+ * @param namespace The namespace where the pod is located
90+ * @returns A promise that resolves with the pod details in JSON format
91+ */
92+ static async getPodDetails (
93+ podName : string ,
94+ namespace : string ,
95+ ) : Promise < string > {
96+ const args = [ "get" , "pod" , podName , "-n" , namespace , "-o" , "json" ] ;
97+ try {
98+ const output = await LogUtils . executeCommand ( "oc" , args ) ;
99+ console . log ( `Details for pod ${ podName } :` , output ) ;
100+ return output ;
101+ } catch ( error ) {
102+ console . error ( `Error fetching details for pod ${ podName } :` , error ) ;
103+ throw new Error ( `Failed to fetch pod details: ${ error } ` ) ;
104+ }
105+ }
106+
107+ /**
108+ * Fetches logs with retry logic in case the log is not immediately available.
73109 *
74110 * @param filter The string to filter the logs
75- * @returns A promise that resolves with the filtered logs
111+ * @param maxRetries Maximum number of retry attempts
112+ * @param retryDelay Delay (in milliseconds) between retries
113+ * @returns The log line matching the filter, or throws an error if not found
76114 */
77- static async getPodLogsWithGrep ( filter : string ) : Promise < string > {
115+ static async getPodLogsWithRetry (
116+ filter : string ,
117+ maxRetries : number = 3 ,
118+ retryDelay : number = 5000 ,
119+ ) : Promise < string > {
78120 const podSelector =
79121 "app.kubernetes.io/component=backstage,app.kubernetes.io/instance=rhdh,app.kubernetes.io/name=backstage" ;
80122 const tailNumber = 30 ;
81- const namespace = process . env . NAME_SPACE || "default " ;
123+ const namespace = process . env . NAME_SPACE || "showcase-ci-nightly " ;
82124
83- const args = [
84- "logs" ,
85- "-l" ,
86- podSelector ,
87- `--tail=${ tailNumber } ` ,
88- "-c" ,
89- "backstage-backend" ,
90- "-n" ,
91- namespace ,
92- ] ;
125+ let attempt = 0 ;
126+ while ( attempt <= maxRetries ) {
127+ try {
128+ console . log (
129+ `Attempt ${ attempt + 1 } /${ maxRetries + 1 } : Fetching logs...` ,
130+ ) ;
131+ const args = [
132+ "logs" ,
133+ "-l" ,
134+ podSelector ,
135+ `--tail=${ tailNumber } ` ,
136+ "-c" ,
137+ "backstage-backend" ,
138+ "-n" ,
139+ namespace ,
140+ ] ;
93141
94- console . log ( "Executing command:" , "oc" , args . join ( " " ) ) ;
142+ console . log ( "Executing command:" , "oc" , args . join ( " " ) ) ;
143+ const output = await LogUtils . executeCommand ( "oc" , args ) ;
95144
96- try {
97- const output = await LogUtils . executeCommand ( "oc" , args ) ;
145+ console . log ( "Raw log output:" , output ) ;
98146
99- const logLines = output . split ( "\n" ) ;
147+ const logLines = output . split ( "\n" ) ;
148+ const filteredLines = logLines . filter ( ( line ) => line . includes ( filter ) ) ;
100149
101- const filteredLines = logLines . filter ( ( line ) => line . includes ( filter ) ) ;
150+ if ( filteredLines . length > 0 ) {
151+ console . log ( "Matching log line found:" , filteredLines [ 0 ] ) ;
152+ return filteredLines [ 0 ] ; // Return the first matching log
153+ }
102154
103- const firstMatch = filteredLines [ 0 ] || "" ;
155+ console . warn (
156+ `No matching logs found for filter "${ filter } " on attempt ${
157+ attempt + 1
158+ } . Retrying...`,
159+ ) ;
160+ } catch ( error ) {
161+ console . error (
162+ `Error fetching logs on attempt ${ attempt + 1 } :` ,
163+ error . message ,
164+ ) ;
165+ }
104166
105- return firstMatch ;
106- } catch ( error ) {
107- console . error ( "Error fetching logs:" , error ) ;
108- throw new Error ( `Failed to fetch logs: ${ error } ` ) ;
167+ attempt ++ ;
168+ if ( attempt <= maxRetries ) {
169+ console . log ( `Waiting ${ retryDelay / 1000 } seconds before retrying...` ) ;
170+ await new Promise ( ( resolve ) => setTimeout ( resolve , retryDelay ) ) ;
171+ }
109172 }
173+
174+ throw new Error (
175+ `Failed to fetch logs for filter "${ filter } " after ${ maxRetries + 1 } attempts.` ,
176+ ) ;
110177 }
111178
112179 /**
@@ -115,8 +182,8 @@ export class LogUtils {
115182 * @returns A promise that resolves when the login is successful
116183 */
117184 static async loginToOpenShift ( ) : Promise < void > {
118- const token = process . env . K8S_CLUSTER_TOKEN ;
119- const server = process . env . K8S_CLUSTER_URL ;
185+ const token = process . env . K8S_CLUSTER_TOKEN || "" ;
186+ const server = process . env . K8S_CLUSTER_URL || "" ;
120187
121188 if ( ! token || ! server ) {
122189 throw new Error (
@@ -155,19 +222,41 @@ export class LogUtils {
155222 baseURL : string ,
156223 plugin : string ,
157224 ) {
158- const actualLog = await LogUtils . getPodLogsWithGrep ( eventName ) ;
159- const expectedLog : Partial < Log > = {
160- actor : {
161- hostname : new URL ( baseURL ) . hostname ,
162- } ,
163- message,
164- plugin,
165- request : {
166- method,
167- url,
168- } ,
169- } ;
170- console . log ( actualLog ) ;
171- LogUtils . validateLog ( JSON . parse ( actualLog ) , expectedLog ) ;
225+ try {
226+ const actualLog = await LogUtils . getPodLogsWithRetry ( eventName ) ;
227+ console . log ( "Raw log output before filtering:" , actualLog ) ;
228+
229+ let parsedLog : Log ;
230+ try {
231+ parsedLog = JSON . parse ( actualLog ) ;
232+ } catch ( parseError ) {
233+ console . error ( "Failed to parse log JSON. Log content:" , actualLog ) ;
234+ throw new Error ( `Invalid JSON received for log: ${ parseError } ` ) ;
235+ }
236+
237+ const expectedLog : Partial < Log > = {
238+ actor : {
239+ hostname : new URL ( baseURL ) . hostname ,
240+ } ,
241+ message,
242+ plugin,
243+ request : {
244+ method,
245+ url,
246+ } ,
247+ } ;
248+
249+ console . log ( "Validating log with expected values:" , expectedLog ) ;
250+ LogUtils . validateLog ( parsedLog , expectedLog ) ;
251+ } catch ( error ) {
252+ console . error ( "Error validating log event:" , error ) ;
253+ console . error ( "Event name:" , eventName ) ;
254+ console . error ( "Expected message:" , message ) ;
255+ console . error ( "Expected method:" , method ) ;
256+ console . error ( "Expected URL:" , url ) ;
257+ console . error ( "Base URL:" , baseURL ) ;
258+ console . error ( "Plugin:" , plugin ) ;
259+ throw error ;
260+ }
172261 }
173262}
0 commit comments