@@ -3,6 +3,29 @@ import { execFile } from "child_process";
3
3
import { Log } from "./logs" ;
4
4
5
5
export 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
+
6
29
/**
7
30
* Validates if the actual log matches the expected log values.
8
31
* It compares both primitive and nested object properties.
@@ -11,7 +34,6 @@ export class LogUtils {
11
34
* @param expected The expected log values to validate against
12
35
*/
13
36
public static validateLog ( actual : Log , expected : Partial < Log > ) {
14
- // Loop through each key in the expected log object
15
37
Object . keys ( expected ) . forEach ( ( key ) => {
16
38
const expectedValue = expected [ key as keyof Log ] ;
17
39
const actualValue = actual [ key as keyof Log ] ;
@@ -42,71 +64,116 @@ export class LogUtils {
42
64
}
43
65
44
66
/**
45
- * Executes a command and returns the output as a promise .
67
+ * Lists all pods in the specified namespace and returns their details .
46
68
*
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
50
71
*/
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
+ }
65
83
}
66
84
67
85
/**
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.
73
109
*
74
110
* @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
76
114
*/
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 > {
78
120
const podSelector =
79
121
"app.kubernetes.io/component=backstage,app.kubernetes.io/instance=rhdh,app.kubernetes.io/name=backstage" ;
80
122
const tailNumber = 30 ;
81
- const namespace = process . env . NAME_SPACE || "default " ;
123
+ const namespace = process . env . NAME_SPACE || "showcase-ci-nightly " ;
82
124
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
+ ] ;
93
141
94
- console . log ( "Executing command:" , "oc" , args . join ( " " ) ) ;
142
+ console . log ( "Executing command:" , "oc" , args . join ( " " ) ) ;
143
+ const output = await LogUtils . executeCommand ( "oc" , args ) ;
95
144
96
- try {
97
- const output = await LogUtils . executeCommand ( "oc" , args ) ;
145
+ console . log ( "Raw log output:" , output ) ;
98
146
99
- const logLines = output . split ( "\n" ) ;
147
+ const logLines = output . split ( "\n" ) ;
148
+ const filteredLines = logLines . filter ( ( line ) => line . includes ( filter ) ) ;
100
149
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
+ }
102
154
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
+ }
104
166
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
+ }
109
172
}
173
+
174
+ throw new Error (
175
+ `Failed to fetch logs for filter "${ filter } " after ${ maxRetries + 1 } attempts.` ,
176
+ ) ;
110
177
}
111
178
112
179
/**
@@ -115,8 +182,8 @@ export class LogUtils {
115
182
* @returns A promise that resolves when the login is successful
116
183
*/
117
184
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 || "" ;
120
187
121
188
if ( ! token || ! server ) {
122
189
throw new Error (
@@ -155,19 +222,41 @@ export class LogUtils {
155
222
baseURL : string ,
156
223
plugin : string ,
157
224
) {
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
+ }
172
261
}
173
262
}
0 commit comments