@@ -7,34 +7,53 @@ import (
77 "path/filepath"
88 "strconv"
99 "strings"
10+ "time"
1011
1112 log "github.com/Sirupsen/logrus"
1213 "github.com/cloudimmunity/go-dockerclientx"
13- )
14+ "github.com/dustin/go-humanize"
1415
15- type Layer struct {
16- Name string
17- Tags []string
18- }
16+ v "github.com/docker-slim/docker-slim/pkg/version"
17+ )
1918
2019// Info represents the reverse engineered Dockerfile info
2120type Info struct {
2221 Lines []string
2322 AllUsers []string
2423 ExeUser string
2524 ExposedPorts []string
26- Layers []Layer
25+ ImageStack []* ImageInfo
26+ }
27+
28+ type ImageInfo struct {
29+ IsTopImage bool `json:"is_top_image"`
30+ ID string `json:"id"`
31+ FullName string `json:"full_name"`
32+ RepoName string `json:"repo_name"`
33+ VersionTag string `json:"version_tag"`
34+ RawTags []string `json:"raw_tags,omitempty"`
35+ CreateTime string `json:"create_time"`
36+ NewSize int64 `json:"new_size"`
37+ NewSizeHuman string `json:"new_size_human"`
38+ BaseImageID string `json:"base_image_id,omitempty"`
39+ Instructions []* InstructionInfo `json:"instructions"`
2740}
2841
29- type imageInst struct {
30- instCmd string
31- instComment string
32- instType string
33- instTime int64
34- layerImageID string
35- imageName string
36- shortTags []string
37- fullTags []string
42+ type InstructionInfo struct {
43+ Type string `json:"type"`
44+ Time string `json:"time"`
45+ IsNop bool `json:"is_nop"`
46+ IsLocal bool `json:"is_local"`
47+ IntermediateImageID string `json:"intermediate_image_id,omitempty"`
48+ Size int64 `json:"size"`
49+ SizeHuman string `json:"size_human,omitempty"`
50+ CommandSnippet string `json:"command_snippet"`
51+ command string
52+ SystemCommands []string `json:"system_commands,omitempty"`
53+ Comment string `json:"comment,omitempty"`
54+ instPosition string
55+ imageFullName string
56+ RawTags []string `json:"raw_tags,omitempty"`
3857}
3958
4059// ReverseDockerfileFromHistory recreates Dockerfile information from container image history
@@ -49,17 +68,26 @@ func ReverseDockerfileFromHistory(apiClient *docker.Client, imageID string) (*In
4968
5069 log .Debugf ("\n \n IMAGE HISTORY =>\n %#v\n \n " , imageHistory )
5170
52- var fatImageDockerInstructions []imageInst
71+ var fatImageDockerInstructions []InstructionInfo
72+ var currentImageInfo * ImageInfo
73+ var prevImageID string
5374
5475 imageLayerCount := len (imageHistory )
5576 imageLayerStart := imageLayerCount - 1
77+ startNewImage := true
5678 if imageLayerCount > 0 {
5779 for idx := imageLayerStart ; idx >= 0 ; idx -- {
80+ isNop := false
81+
5882 nopPrefix := "/bin/sh -c #(nop) "
5983 execPrefix := "/bin/sh -c "
6084 rawLine := imageHistory [idx ].CreatedBy
6185 var inst string
6286
87+ if strings .Contains (rawLine , "(nop)" ) {
88+ isNop = true
89+ }
90+
6391 switch {
6492 case len (rawLine ) == 0 :
6593 inst = "FROM scratch"
@@ -116,60 +144,124 @@ func ReverseDockerfileFromHistory(apiClient *docker.Client, imageID string) (*In
116144 }
117145 }
118146
119- instInfo := imageInst {
120- instCmd : inst ,
121- instTime : imageHistory [idx ].Created ,
122- layerImageID : imageHistory [idx ].ID ,
123- instComment : imageHistory [idx ].Comment ,
147+ instInfo := InstructionInfo {
148+ IsNop : isNop ,
149+ command : cleanInst ,
150+ Time : time .Unix (imageHistory [idx ].Created , 0 ).UTC ().Format (time .RFC3339 ),
151+ Comment : imageHistory [idx ].Comment ,
152+ RawTags : imageHistory [idx ].Tags ,
153+ Size : imageHistory [idx ].Size ,
154+ }
155+
156+ instParts := strings .SplitN (cleanInst , " " , 2 )
157+ if len (instParts ) == 2 {
158+ instInfo .Type = instParts [0 ]
159+ }
160+
161+ if instInfo .Type == "RUN" {
162+ var cmdParts []string
163+ cmds := strings .Replace (instParts [1 ], "\\ " , "" , - 1 )
164+ if strings .Contains (cmds , "&&" ) {
165+ cmdParts = strings .Split (cmds , "&&" )
166+ } else {
167+ cmdParts = strings .Split (cmds , ";" )
168+ }
169+
170+ for _ , cmd := range cmdParts {
171+ cmd = strings .TrimSpace (cmd )
172+ cmd = strings .Replace (cmd , "\t " , "" , - 1 )
173+ cmd = strings .Replace (cmd , "\n " , "" , - 1 )
174+ instInfo .SystemCommands = append (instInfo .SystemCommands , cmd )
175+ }
176+ }
177+
178+ if instInfo .Type == "WORKDIR" {
179+ instInfo .SystemCommands = append (instInfo .SystemCommands , fmt .Sprintf ("mkdir -p %s" , instParts [1 ]))
180+ }
181+
182+ if len (instInfo .command ) > 44 {
183+ instInfo .CommandSnippet = fmt .Sprintf ("%s..." , instInfo .command [0 :44 ])
184+ } else {
185+ instInfo .CommandSnippet = instInfo .command
186+ }
187+
188+ if instInfo .Size > 0 {
189+ instInfo .SizeHuman = humanize .Bytes (uint64 (instInfo .Size ))
190+ }
191+
192+ if imageHistory [idx ].ID != "<missing>" {
193+ instInfo .IsLocal = true
194+ instInfo .IntermediateImageID = imageHistory [idx ].ID
124195 }
125196
126- instType := "intermediate"
197+ if startNewImage {
198+ startNewImage = false
199+ currentImageInfo = & ImageInfo {
200+ BaseImageID : prevImageID ,
201+ NewSize : 0 ,
202+ }
203+ }
204+
205+ currentImageInfo .NewSize += imageHistory [idx ].Size
206+ currentImageInfo .Instructions = append (currentImageInfo .Instructions , & instInfo )
207+
208+ instPosition := "intermediate"
127209 if idx == imageLayerStart {
128- instType = "first"
210+ instPosition = "first" //first instruction in the list
129211 }
130212
131213 if len (imageHistory [idx ].Tags ) > 0 {
132- instType = "last"
214+ instPosition = "last" //last in an image
133215
134- if tagInfo := strings .Split (imageHistory [idx ].Tags [0 ], ":" ); len (tagInfo ) > 1 {
135- instInfo .imageName = tagInfo [0 ]
136- }
216+ currentImageInfo .ID = imageHistory [idx ].ID
217+ prevImageID = currentImageInfo .ID
137218
138- instInfo .fullTags = imageHistory [idx ].Tags
219+ currentImageInfo .CreateTime = instInfo .Time
220+ currentImageInfo .RawTags = imageHistory [idx ].Tags
139221
140- for _ , fullTag := range instInfo .fullTags {
141- if tagInfo := strings .Split (fullTag , ":" ); len (tagInfo ) > 1 {
142- instInfo .shortTags = append (instInfo .shortTags , tagInfo [1 ])
143- }
222+ instInfo .imageFullName = imageHistory [idx ].Tags [0 ]
223+ currentImageInfo .FullName = imageHistory [idx ].Tags [0 ]
224+
225+ if tagInfo := strings .Split (imageHistory [idx ].Tags [0 ], ":" ); len (tagInfo ) > 1 {
226+ currentImageInfo .RepoName = tagInfo [0 ]
227+ currentImageInfo .VersionTag = tagInfo [1 ]
144228 }
145229
146- out .Layers = append (out .Layers , Layer {Name : instInfo .imageName , Tags : instInfo .shortTags })
230+ currentImageInfo .NewSizeHuman = humanize .Bytes (uint64 (currentImageInfo .NewSize ))
231+
232+ out .ImageStack = append (out .ImageStack , currentImageInfo )
233+ startNewImage = true
147234 }
148235
149- instInfo .instType = instType
236+ instInfo .instPosition = instPosition
150237
151238 fatImageDockerInstructions = append (fatImageDockerInstructions , instInfo )
152239 }
240+
241+ if currentImageInfo != nil {
242+ currentImageInfo .IsTopImage = true
243+ }
153244 }
154245
155246 for idx , instInfo := range fatImageDockerInstructions {
156- if instInfo .instType == "first" {
247+ if instInfo .instPosition == "first" {
157248 out .Lines = append (out .Lines , "# new image" )
158249 }
159250
160- out .Lines = append (out .Lines , instInfo .instCmd )
161- if instInfo .instType == "last" {
251+ out .Lines = append (out .Lines , instInfo .command )
252+ if instInfo .instPosition == "last" {
162253 commentText := fmt .Sprintf ("# end of image: %s (id: %s tags: %s)" ,
163- instInfo .imageName , instInfo .layerImageID , strings .Join (instInfo .shortTags , "," ))
254+ instInfo .imageFullName , instInfo .IntermediateImageID , strings .Join (instInfo .RawTags , "," ))
255+
164256 out .Lines = append (out .Lines , commentText )
165257 out .Lines = append (out .Lines , "" )
166258 if idx < (len (fatImageDockerInstructions ) - 1 ) {
167259 out .Lines = append (out .Lines , "# new image" )
168260 }
169261 }
170262
171- if instInfo .instComment != "" {
172- out .Lines = append (out .Lines , "# " + instInfo .instComment )
263+ if instInfo .Comment != "" {
264+ out .Lines = append (out .Lines , "# " + instInfo .Comment )
173265 }
174266
175267 //TODO: use time diff to separate each instruction
@@ -228,6 +320,9 @@ func GenerateFromInfo(location string,
228320 var dfData bytes.Buffer
229321 dfData .WriteString ("FROM scratch\n " )
230322
323+ dsInfoLabel := fmt .Sprintf ("LABEL docker-slim.version=\" %s\" \n " , v .Current ())
324+ dfData .WriteString (dsInfoLabel )
325+
231326 if len (volumes ) > 0 {
232327 var volumeList []string
233328 for volumeName := range volumes {
0 commit comments