55package content
66
77import (
8+ "bufio"
89 "bytes"
910 "context"
1011 "encoding/json"
1112 "errors"
13+ "fmt"
1214 "io/ioutil"
1315 "os"
1416 "os/exec"
@@ -122,21 +124,22 @@ func CollectRemoteContent(ctx context.Context, rs storage.DirectAccess, ps stora
122124}
123125
124126// RunInitializer runs a content initializer in a user, PID and mount namespace to isolate it from ws-daemon
125- func RunInitializer (ctx context.Context , destination string , initializer * csapi.WorkspaceInitializer , remoteContent map [string ]storage.DownloadInfo , opts RunInitializerOpts ) (err error ) {
127+ func RunInitializer (ctx context.Context , destination string , initializer * csapi.WorkspaceInitializer , remoteContent map [string ]storage.DownloadInfo , opts RunInitializerOpts ) (* csapi. InitializerMetrics , error ) {
126128 //nolint:ineffassign,staticcheck
127129 span , ctx := opentracing .StartSpanFromContext (ctx , "RunInitializer" )
130+ var err error
128131 defer tracing .FinishSpan (span , & err )
129132
130133 // it's possible the destination folder doesn't exist yet, because the kubelet hasn't created it yet.
131134 // If we fail to create the folder, it either already exists, or we'll fail when we try and mount it.
132135 err = os .MkdirAll (destination , 0755 )
133136 if err != nil && ! os .IsExist (err ) {
134- return xerrors .Errorf ("cannot mkdir destination: %w" , err )
137+ return nil , xerrors .Errorf ("cannot mkdir destination: %w" , err )
135138 }
136139
137140 init , err := proto .Marshal (initializer )
138141 if err != nil {
139- return err
142+ return nil , err
140143 }
141144
142145 if opts .GID == 0 {
@@ -148,13 +151,13 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
148151
149152 tmpdir , err := os .MkdirTemp ("" , "content-init" )
150153 if err != nil {
151- return err
154+ return nil , err
152155 }
153156 defer os .RemoveAll (tmpdir )
154157
155158 err = os .MkdirAll (filepath .Join (tmpdir , "rootfs" ), 0755 )
156159 if err != nil {
157- return err
160+ return nil , err
158161 }
159162
160163 msg := msgInitContent {
@@ -169,11 +172,11 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
169172 }
170173 fc , err := json .MarshalIndent (msg , "" , " " )
171174 if err != nil {
172- return err
175+ return nil , err
173176 }
174177 err = os .WriteFile (filepath .Join (tmpdir , "rootfs" , "content.json" ), fc , 0644 )
175178 if err != nil {
176- return err
179+ return nil , err
177180 }
178181
179182 spec := specconv .Example ()
@@ -226,11 +229,11 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
226229
227230 fc , err = json .MarshalIndent (spec , "" , " " )
228231 if err != nil {
229- return err
232+ return nil , err
230233 }
231234 err = os .WriteFile (filepath .Join (tmpdir , "config.json" ), fc , 0644 )
232235 if err != nil {
233- return err
236+ return nil , err
234237 }
235238
236239 args := []string {"--root" , "state" }
@@ -243,7 +246,7 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
243246 if opts .OWI .InstanceID == "" {
244247 id , err := uuid .NewRandom ()
245248 if err != nil {
246- return err
249+ return nil , err
247250 }
248251 name = "init-rnd-" + id .String ()
249252 } else {
@@ -256,7 +259,7 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
256259
257260 errIn , errOut , err := os .Pipe ()
258261 if err != nil {
259- return err
262+ return nil , err
260263 }
261264 errch := make (chan []byte , 1 )
262265 go func () {
@@ -286,27 +289,49 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
286289 // The program has exited with an exit code != 0. If it's FAIL_CONTENT_INITIALIZER_EXIT_CODE, it was deliberate.
287290 if status , ok := exiterr .Sys ().(syscall.WaitStatus ); ok && status .ExitStatus () == FAIL_CONTENT_INITIALIZER_EXIT_CODE {
288291 log .WithError (err ).WithFields (opts .OWI .Fields ()).WithField ("errmsgsize" , len (errmsg )).WithField ("exitCode" , status .ExitStatus ()).WithField ("args" , args ).Error ("content init failed" )
289- return xerrors .Errorf (string (errmsg ))
292+ return nil , xerrors .Errorf (string (errmsg ))
290293 }
291294 }
292295
293- return err
296+ return nil , err
294297 }
295298
299+ stats := parseStats (& cmdOut )
300+
301+ return stats , nil
302+ }
303+
304+ func parseStats (buf * bytes.Buffer ) * csapi.InitializerMetrics {
305+ scanner := bufio .NewScanner (buf )
306+ for scanner .Scan () {
307+ line := string (scanner .Bytes ())
308+ if ! strings .HasPrefix (line , STATS_PREFIX ) {
309+ continue
310+ }
311+
312+ b := strings .TrimSpace (strings .TrimPrefix (line , STATS_PREFIX ))
313+ var stats csapi.InitializerMetrics
314+ err := json .Unmarshal ([]byte (b ), & stats )
315+ if err != nil {
316+ log .WithError (err ).WithField ("line" , line ).Error ("cannot unmarshal stats" )
317+ return nil
318+ }
319+ return & stats
320+ }
296321 return nil
297322}
298323
299324// RunInitializerChild is the function that's expected to run when we call `/proc/self/exe content-initializer`
300- func RunInitializerChild () (err error ) {
325+ func RunInitializerChild () (serializedStats [] byte , err error ) {
301326 fc , err := os .ReadFile ("/content.json" )
302327 if err != nil {
303- return err
328+ return nil , err
304329 }
305330
306331 var initmsg msgInitContent
307332 err = json .Unmarshal (fc , & initmsg )
308333 if err != nil {
309- return err
334+ return nil , err
310335 }
311336 log .Log = logrus .WithFields (initmsg .OWI )
312337
@@ -323,15 +348,15 @@ func RunInitializerChild() (err error) {
323348 var req csapi.WorkspaceInitializer
324349 err = proto .Unmarshal (initmsg .Initializer , & req )
325350 if err != nil {
326- return err
351+ return nil , err
327352 }
328353
329354 rs := & remoteContentStorage {RemoteContent : initmsg .RemoteContent }
330355
331356 dst := initmsg .Destination
332357 initializer , err := wsinit .NewFromRequest (ctx , dst , rs , & req , wsinit.NewFromRequestOpts {ForceGitpodUserForGit : false })
333358 if err != nil {
334- return err
359+ return nil , err
335360 }
336361
337362 initSource , stats , err := wsinit .InitializeWorkspace (ctx , dst , rs ,
@@ -341,23 +366,35 @@ func RunInitializerChild() (err error) {
341366 wsinit .WithCleanSlate ,
342367 )
343368 if err != nil {
344- return err
369+ return nil , err
345370 }
346371
347372 // some workspace content may have a `/dst/.gitpod` file or directory. That would break
348373 // the workspace ready file placement (see https://github.com/gitpod-io/gitpod/issues/7694).
349374 err = wsinit .EnsureCleanDotGitpodDirectory (ctx , dst )
350375 if err != nil {
351- return err
376+ return nil , err
352377 }
353378
354379 // Place the ready file to make Theia "open its gates"
355380 err = wsinit .PlaceWorkspaceReadyFile (ctx , dst , initSource , stats , initmsg .UID , initmsg .GID )
356381 if err != nil {
357- return err
382+ return nil , err
358383 }
359384
360- return nil
385+ // Serialize metrics, so we can pass them back to the caller
386+ serializedStats , err = json .Marshal (stats )
387+ if err != nil {
388+ return nil , err
389+ }
390+
391+ return serializedStats , nil
392+ }
393+
394+ const STATS_PREFIX = "STATS:"
395+
396+ func FormatStatsBytes (statsBytes []byte ) string {
397+ return fmt .Sprintf ("%s %s\n " , STATS_PREFIX , string (statsBytes ))
361398}
362399
363400var _ storage.DirectAccess = & remoteContentStorage {}
0 commit comments