@@ -46,7 +46,7 @@ import { WorkspaceDeletionService } from './workspace-deletion-service';
46
46
import { WorkspaceFactory } from './workspace-factory' ;
47
47
import { WorkspaceStarter } from './workspace-starter' ;
48
48
import { HeadlessLogUrls } from "@gitpod/gitpod-protocol/lib/headless-workspace-log" ;
49
- import { HeadlessLogService } from "./headless-log-service" ;
49
+ import { HeadlessLogService , HeadlessLogEndpoint } from "./headless-log-service" ;
50
50
import { InvalidGitpodYMLError } from "./config-provider" ;
51
51
import { ProjectsService } from "../projects/projects-service" ;
52
52
import { LocalMessageBroker } from "../messaging/local-message-broker" ;
@@ -58,6 +58,7 @@ import { ClientMetadata } from '../websocket/websocket-connection-manager';
58
58
import { ConfigurationService } from '../config/configuration-service' ;
59
59
import { ProjectEnvVar } from '@gitpod/gitpod-protocol/src/protocol' ;
60
60
import { InstallationAdminSettings } from '@gitpod/gitpod-protocol' ;
61
+ import { Deferred } from '@gitpod/gitpod-protocol/lib/util/deferred' ;
61
62
62
63
// shortcut
63
64
export const traceWI = ( ctx : TraceContext , wi : Omit < LogContext , "userId" > ) => TraceContext . setOWI ( ctx , wi ) ; // userId is already taken care of in WebsocketConnectionManager
@@ -1112,24 +1113,90 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
1112
1113
traceWI ( ctx , { workspaceId } ) ;
1113
1114
1114
1115
const user = this . checkAndBlockUser ( "watchWorkspaceImageBuildLogs" , undefined , { workspaceId } ) ;
1115
- const logCtx : LogContext = { userId : user . id , workspaceId } ;
1116
-
1117
- const { instance, workspace } = await this . internGetCurrentWorkspaceInstance ( ctx , workspaceId ) ;
1118
- if ( ! this . client ) {
1116
+ const client = this . client ;
1117
+ if ( ! client ) {
1119
1118
return ;
1120
1119
}
1120
+
1121
+ const logCtx : LogContext = { userId : user . id , workspaceId } ;
1122
+ let { instance, workspace } = await this . internGetCurrentWorkspaceInstance ( ctx , workspaceId ) ;
1121
1123
if ( ! instance ) {
1122
1124
log . debug ( logCtx , `No running instance for workspaceId.` ) ;
1123
1125
return ;
1124
1126
}
1125
1127
traceWI ( ctx , { instanceId : instance . id } ) ;
1126
- if ( ! workspace . imageNameResolved ) {
1127
- log . debug ( logCtx , `No imageNameResolved set for workspaceId, cannot watch logs.` ) ;
1128
- return ;
1129
- }
1130
1128
const teamMembers = await this . getTeamMembersByProject ( workspace . projectId ) ;
1131
1129
await this . guardAccess ( { kind : "workspaceInstance" , subject : instance , workspace, teamMembers } , "get" ) ;
1132
- if ( ! this . client ) {
1130
+
1131
+ // wait for up to 20s for imageBuildLogInfo to appear due to:
1132
+ // - db-sync round-trip times
1133
+ // - but also: wait until the image build actually started (image pull!), and log info is available!
1134
+ for ( let i = 0 ; i < 10 ; i ++ ) {
1135
+ if ( ! instance || instance . status . phase !== 'preparing' ) {
1136
+ log . debug ( logCtx , `imagebuild logs: instance is not/no longer in 'preparing' state` , { phase : instance ?. status . phase } ) ;
1137
+ return ;
1138
+ }
1139
+ if ( workspace . imageBuildInfo ?. log ) {
1140
+ break ;
1141
+ }
1142
+ await new Promise ( resolve => setTimeout ( resolve , 2000 ) ) ;
1143
+
1144
+ ( { instance, workspace } = await this . internGetCurrentWorkspaceInstance ( ctx , workspaceId ) ) ;
1145
+ if ( ! workspace ) {
1146
+ log . warn ( logCtx , `no workspace for workspaceId.` ) ;
1147
+ return ;
1148
+ }
1149
+ }
1150
+
1151
+ if ( ! workspace . imageBuildInfo ?. log ) {
1152
+ // during roll-out this is our fall-back case.
1153
+ // Afterwards we might want to do some spinning-lock and re-check for a certain period (30s?) to give db-sync
1154
+ // a change to move the imageBuildLogInfo across the globe.
1155
+
1156
+ log . warn ( logCtx , "imageBuild logs: fallback!" ) ;
1157
+ ctx . span ?. setTag ( "workspace.imageBuild.logs.fallback" , true ) ;
1158
+ await this . deprecatedDoWatchWorkspaceImageBuildLogs ( ctx , logCtx , workspace ) ;
1159
+ return ;
1160
+ }
1161
+ const logInfo = workspace . imageBuildInfo . log ;
1162
+
1163
+ const aborted = new Deferred < boolean > ( ) ;
1164
+ try {
1165
+ const logEndpoint : HeadlessLogEndpoint = {
1166
+ url : logInfo . url ,
1167
+ headers : logInfo . headers ,
1168
+ } ;
1169
+ let lineCount = 0 ;
1170
+ await this . headlessLogService . streamImageBuildLog ( logCtx , logEndpoint , async ( chunk ) => {
1171
+ if ( aborted . isResolved ) {
1172
+ return ;
1173
+ }
1174
+
1175
+ try {
1176
+ chunk = chunk . replace ( "\n" , WorkspaceImageBuild . LogLine . DELIMITER ) ;
1177
+ lineCount += chunk . split ( WorkspaceImageBuild . LogLine . DELIMITER_REGEX ) . length ;
1178
+
1179
+ client . onWorkspaceImageBuildLogs ( undefined as any , {
1180
+ text : chunk ,
1181
+ isDiff : true ,
1182
+ upToLine : lineCount
1183
+ } ) ;
1184
+ } catch ( err ) {
1185
+ log . error ( "error while streaming imagebuild logs" , err ) ;
1186
+ aborted . resolve ( true ) ;
1187
+ }
1188
+ } , aborted ) ;
1189
+ } catch ( err ) {
1190
+ log . error ( logCtx , "cannot watch imagebuild logs for workspaceId" , err ) ;
1191
+ throw new ResponseError ( ErrorCodes . HEADLESS_LOG_NOT_YET_AVAILABLE , "cannot watch imagebuild logs for workspaceId" ) ;
1192
+ } finally {
1193
+ aborted . resolve ( false ) ;
1194
+ }
1195
+ }
1196
+
1197
+ protected async deprecatedDoWatchWorkspaceImageBuildLogs ( ctx : TraceContext , logCtx : LogContext , workspace : Workspace ) {
1198
+ if ( ! workspace . imageNameResolved ) {
1199
+ log . debug ( logCtx , `No imageNameResolved set for workspaceId, cannot watch logs.` ) ;
1133
1200
return ;
1134
1201
}
1135
1202
0 commit comments