@@ -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
@@ -1113,24 +1114,87 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
1113
1114
traceWI ( ctx , { workspaceId } ) ;
1114
1115
1115
1116
const user = this . checkAndBlockUser ( "watchWorkspaceImageBuildLogs" , undefined , { workspaceId } ) ;
1116
- const logCtx : LogContext = { userId : user . id , workspaceId } ;
1117
-
1118
- const { instance, workspace } = await this . internGetCurrentWorkspaceInstance ( ctx , workspaceId ) ;
1119
- if ( ! this . client ) {
1117
+ const client = this . client ;
1118
+ if ( ! client ) {
1120
1119
return ;
1121
1120
}
1121
+
1122
+ const logCtx : LogContext = { userId : user . id , workspaceId } ;
1123
+ let { instance, workspace } = await this . internGetCurrentWorkspaceInstance ( ctx , workspaceId ) ;
1122
1124
if ( ! instance ) {
1123
1125
log . debug ( logCtx , `No running instance for workspaceId.` ) ;
1124
1126
return ;
1125
1127
}
1126
1128
traceWI ( ctx , { instanceId : instance . id } ) ;
1127
- if ( ! workspace . imageNameResolved ) {
1128
- log . debug ( logCtx , `No imageNameResolved set for workspaceId, cannot watch logs.` ) ;
1129
- return ;
1130
- }
1131
1129
const teamMembers = await this . getTeamMembersByProject ( workspace . projectId ) ;
1132
1130
await this . guardAccess ( { kind : "workspaceInstance" , subject : instance , workspace, teamMembers } , "get" ) ;
1133
- if ( ! this . client ) {
1131
+
1132
+ // wait for up to 20s for imageBuildLogInfo to appear due to:
1133
+ // - db-sync round-trip times
1134
+ // - but also: wait until the image build actually started (image pull!), and log info is available!
1135
+ for ( let i = 0 ; i < 10 ; i ++ ) {
1136
+ if ( instance . imageBuildInfo ?. log ) {
1137
+ break ;
1138
+ }
1139
+ await new Promise ( resolve => setTimeout ( resolve , 2000 ) ) ;
1140
+
1141
+ const wsi = await this . workspaceDb . trace ( ctx ) . findInstanceById ( instance . id ) ;
1142
+ if ( ! wsi || wsi . status . phase !== 'preparing' ) {
1143
+ log . debug ( logCtx , `imagebuild logs: instance is not/no longer in 'preparing' state` , { phase : wsi ?. status . phase } ) ;
1144
+ return ;
1145
+ }
1146
+ instance = wsi as WorkspaceInstance ; // help the compiler a bit
1147
+ }
1148
+
1149
+ const logInfo = instance . imageBuildInfo ?. log ;
1150
+ if ( ! logInfo ) {
1151
+ // during roll-out this is our fall-back case.
1152
+ // Afterwards we might want to do some spinning-lock and re-check for a certain period (30s?) to give db-sync
1153
+ // a change to move the imageBuildLogInfo across the globe.
1154
+
1155
+ log . warn ( logCtx , "imageBuild logs: fallback!" ) ;
1156
+ ctx . span ?. setTag ( "workspace.imageBuild.logs.fallback" , true ) ;
1157
+ await this . deprecatedDoWatchWorkspaceImageBuildLogs ( ctx , logCtx , workspace ) ;
1158
+ return ;
1159
+ }
1160
+
1161
+ const aborted = new Deferred < boolean > ( ) ;
1162
+ try {
1163
+ const logEndpoint : HeadlessLogEndpoint = {
1164
+ url : logInfo . url ,
1165
+ headers : logInfo . headers ,
1166
+ } ;
1167
+ let lineCount = 0 ;
1168
+ await this . headlessLogService . streamImageBuildLog ( logCtx , logEndpoint , async ( chunk ) => {
1169
+ if ( aborted . isResolved ) {
1170
+ return ;
1171
+ }
1172
+
1173
+ try {
1174
+ chunk = chunk . replace ( "\n" , WorkspaceImageBuild . LogLine . DELIMITER ) ;
1175
+ lineCount += chunk . split ( WorkspaceImageBuild . LogLine . DELIMITER_REGEX ) . length ;
1176
+
1177
+ client . onWorkspaceImageBuildLogs ( undefined as any , {
1178
+ text : chunk ,
1179
+ isDiff : true ,
1180
+ upToLine : lineCount
1181
+ } ) ;
1182
+ } catch ( err ) {
1183
+ log . error ( "error while streaming imagebuild logs" , err ) ;
1184
+ aborted . resolve ( true ) ;
1185
+ }
1186
+ } , aborted ) ;
1187
+ } catch ( err ) {
1188
+ log . error ( logCtx , "cannot watch imagebuild logs for workspaceId" , err ) ;
1189
+ throw new ResponseError ( ErrorCodes . HEADLESS_LOG_NOT_YET_AVAILABLE , "cannot watch imagebuild logs for workspaceId" ) ;
1190
+ } finally {
1191
+ aborted . resolve ( false ) ;
1192
+ }
1193
+ }
1194
+
1195
+ protected async deprecatedDoWatchWorkspaceImageBuildLogs ( ctx : TraceContext , logCtx : LogContext , workspace : Workspace ) {
1196
+ if ( ! workspace . imageNameResolved ) {
1197
+ log . debug ( logCtx , `No imageNameResolved set for workspaceId, cannot watch logs.` ) ;
1134
1198
return ;
1135
1199
}
1136
1200
0 commit comments