Skip to content

Commit cbfe4b4

Browse files
committed
Add dockerfileSnippet action
1 parent c0cea64 commit cbfe4b4

3 files changed

Lines changed: 172 additions & 63 deletions

File tree

internal/pkg/devcontainers/snippet.go

Lines changed: 107 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,16 @@ type DevcontainerSnippet struct {
4040
type FolderSnippetActionType string
4141

4242
const (
43-
FolderSnippetActionMergeJSON FolderSnippetActionType = "mergeJSON" // merge JSON file from snippet with target JSON file
44-
FolderSnippetActionCopyAndRun FolderSnippetActionType = "copyAndRun" // COPY and RUN script from snippet in the Dockerfile (as with single-file snippet)
43+
FolderSnippetActionMergeJSON FolderSnippetActionType = "mergeJSON" // merge JSON file from snippet with target JSON file
44+
FolderSnippetActionCopyAndRun FolderSnippetActionType = "copyAndRun" // COPY and RUN script from snippet in the Dockerfile (as with single-file snippet)
45+
FolderSnippetActionDockerfileSnippet FolderSnippetActionType = "dockerfileSnippet" // snippet to include as-is in the Dockerfile
4546
)
4647

4748
type FolderSnippetAction struct {
4849
Type FolderSnippetActionType `json:"type"`
49-
SourcePath string `json:"source"` // for mergeJSON this is snippet-relative path to JSON. for copyAndRun this is the script filename
50-
TargetPath string `json:"target"` // for mergeJSON this is project-relative path to JSON
50+
SourcePath string `json:"source"` // for mergeJSON this is snippet-relative path to JSON. for copyAndRun this is the script filename
51+
TargetPath string `json:"target"` // for mergeJSON this is project-relative path to JSON
52+
Content string `json:"content"` // for dockerfileSnippet this is the content to include
5153
}
5254

5355
// FolderSnippet maps to the content of the snippet.json file for folder-based snippets
@@ -177,6 +179,64 @@ func addSingleFileSnippetToDevContainer(projectFolder string, snippet *Devcontai
177179
err := copyAndRunScriptFile(projectFolder, snippet, snippetBasePath, scriptFolderPath, scriptFilename)
178180
return err
179181
}
182+
183+
func addFolderSnippetToDevContainer(projectFolder string, snippet *DevcontainerSnippet) error {
184+
if snippet.Type != DevcontainerSnippetTypeFolder {
185+
return fmt.Errorf("Expected folder snippet")
186+
}
187+
188+
snippetJSONPath := filepath.Join(snippet.Path, "snippet.json")
189+
buf, err := ioutil.ReadFile(snippetJSONPath)
190+
if err != nil {
191+
return err
192+
}
193+
var snippetJSON FolderSnippet
194+
err = json.Unmarshal(buf, &snippetJSON)
195+
if err != nil {
196+
return err
197+
}
198+
199+
for _, action := range snippetJSON.Actions {
200+
switch action.Type {
201+
case FolderSnippetActionMergeJSON:
202+
if action.SourcePath == "" {
203+
return fmt.Errorf("source must be set for %s actions", action.Type)
204+
}
205+
if action.TargetPath == "" {
206+
return fmt.Errorf("target must be set for %s actions", action.Type)
207+
}
208+
err = mergeJSON(projectFolder, snippet, action.SourcePath, action.TargetPath)
209+
if err != nil {
210+
return err
211+
}
212+
case FolderSnippetActionCopyAndRun:
213+
if action.SourcePath == "" {
214+
return fmt.Errorf("source must be set for %s actions", action.Type)
215+
}
216+
targetPath := filepath.Join(projectFolder, ".devcontainer", "scripts")
217+
sourceParent, sourceFileName := filepath.Split(action.SourcePath)
218+
sourceBasePath := filepath.Join(snippet.Path, sourceParent)
219+
err = copyAndRunScriptFile(projectFolder, snippet, sourceBasePath, targetPath, sourceFileName)
220+
if err != nil {
221+
return err
222+
}
223+
case FolderSnippetActionDockerfileSnippet:
224+
if action.Content == "" {
225+
return fmt.Errorf("content must be set for %s actions", action.Type)
226+
}
227+
dockerfileFilename := filepath.Join(projectFolder, ".devcontainer", "Dockerfile")
228+
err = insertDockerfileSnippet(dockerfileFilename, action.Content+"\n")
229+
if err != nil {
230+
return err
231+
}
232+
default:
233+
return fmt.Errorf("unhandled action type: %q", action.Type)
234+
}
235+
}
236+
237+
return nil
238+
}
239+
180240
func copyAndRunScriptFile(projectFolder string, snippet *DevcontainerSnippet, snippetBasePath string, targetPath, scriptFilename string) error {
181241
if err := os.MkdirAll(targetPath, 0755); err != nil {
182242
return err
@@ -185,47 +245,57 @@ func copyAndRunScriptFile(projectFolder string, snippet *DevcontainerSnippet, sn
185245
return err
186246
}
187247

248+
snippetContent := fmt.Sprintf(`# %[1]s
249+
COPY scripts/%[2]s /tmp/
250+
RUN /tmp/%[2]s
251+
`, snippet.Name, scriptFilename)
188252
dockerfileFilename := filepath.Join(projectFolder, ".devcontainer", "Dockerfile")
253+
254+
err := insertDockerfileSnippet(dockerfileFilename, snippetContent)
255+
return err
256+
}
257+
258+
func insertDockerfileSnippet(dockerfileFilename string, snippetContent string) error {
259+
189260
buf, err := ioutil.ReadFile(dockerfileFilename)
190261
if err != nil {
191262
return fmt.Errorf("Error reading Dockerfile: %s", err)
192263
}
193264

194-
snippetContent := fmt.Sprintf(`
195-
# %[1]s
196-
COPY scripts/%[2]s /tmp/
197-
RUN /tmp/%[2]s
198-
`, snippet.Name, scriptFilename)
199-
200265
dockerfileContent := string(buf)
201266
dockerFileLines := strings.Split(dockerfileContent, "\n")
202267
addSeparator := false
203268
addedSnippetContent := false
204269
var newContent bytes.Buffer
205270
for _, line := range dockerFileLines {
271+
if addSeparator {
272+
if _, err = newContent.WriteString("\n"); err != nil {
273+
return err
274+
}
275+
}
276+
addSeparator = true
277+
206278
if strings.Contains(line, "__DEVCONTAINER_SNIPPET_INSERT__") {
207279
if _, err = newContent.WriteString(snippetContent); err != nil {
208280
return err
209281
}
210282
if _, err = newContent.WriteString("\n"); err != nil {
211283
return err
212284
}
285+
line += "\n"
213286
addedSnippetContent = true
214287
addSeparator = false // avoid extra separator
215288
}
216289

217-
if addSeparator {
218-
if _, err = newContent.WriteString("\n"); err != nil {
219-
return err
220-
}
221-
}
222-
addSeparator = true
223290
if _, err = newContent.WriteString(line); err != nil {
224291
return err
225292
}
226293
}
227294

228295
if !addedSnippetContent {
296+
if _, err = newContent.WriteString("\n"); err != nil {
297+
return err
298+
}
229299
if _, err = newContent.WriteString(snippetContent); err != nil {
230300
return err
231301
}
@@ -234,47 +304,8 @@ RUN /tmp/%[2]s
234304
err = ioutil.WriteFile(dockerfileFilename, newContent.Bytes(), 0)
235305

236306
return err
237-
}
238307

239-
func addFolderSnippetToDevContainer(projectFolder string, snippet *DevcontainerSnippet) error {
240-
if snippet.Type != DevcontainerSnippetTypeFolder {
241-
return fmt.Errorf("Expected folder snippet")
242-
}
243-
244-
snippetJSONPath := filepath.Join(snippet.Path, "snippet.json")
245-
buf, err := ioutil.ReadFile(snippetJSONPath)
246-
if err != nil {
247-
return err
248-
}
249-
var snippetJSON FolderSnippet
250-
err = json.Unmarshal(buf, &snippetJSON)
251-
if err != nil {
252-
return err
253-
}
254-
255-
for _, action := range snippetJSON.Actions {
256-
switch action.Type {
257-
case FolderSnippetActionMergeJSON:
258-
err = mergeJSON(projectFolder, snippet, action.SourcePath, action.TargetPath)
259-
if err != nil {
260-
return err
261-
}
262-
case FolderSnippetActionCopyAndRun:
263-
targetPath := filepath.Join(projectFolder, ".devcontainer", "scripts")
264-
sourceParent, sourceFileName := filepath.Split(action.SourcePath)
265-
sourceBasePath := filepath.Join(snippet.Path, sourceParent)
266-
err = copyAndRunScriptFile(projectFolder, snippet, sourceBasePath, targetPath, sourceFileName)
267-
if err != nil {
268-
return err
269-
}
270-
default:
271-
return fmt.Errorf("unhandled action type: %q", action.Type)
272-
}
273-
}
274-
275-
return nil
276308
}
277-
278309
func mergeJSON(projectFolder string, snippet *DevcontainerSnippet, relativeMergePath string, relativeBasePath string) error {
279310
mergePath := filepath.Join(snippet.Path, relativeMergePath)
280311
_, err := os.Stat(mergePath)
@@ -300,11 +331,20 @@ func mergeJSON(projectFolder string, snippet *DevcontainerSnippet, relativeMerge
300331
resultJSON, err := dora_ast.WriteJSONString(resultDocument)
301332

302333
// replace __DEVCONTAINER_NAME__ with name
303-
devcontainerName, err := getDevcontainerName(filepath.Join(projectFolder, ".devcontainer/devcontainer.json"))
304-
if err != nil {
305-
return err
334+
devcontainerName, devcontainerUserName := getDevcontainerNameAndUserName(filepath.Join(projectFolder, ".devcontainer/devcontainer.json"))
335+
if devcontainerName == "" {
336+
return fmt.Errorf("failed to get dev container name")
306337
}
307338
resultJSON = strings.ReplaceAll(resultJSON, "__DEVCONTAINER_NAME__", devcontainerName)
339+
if devcontainerUserName == "" {
340+
devcontainerUserName = "root"
341+
}
342+
devcontainerHome := "/home/" + devcontainerUserName
343+
if devcontainerUserName == "root" {
344+
devcontainerHome = "/root"
345+
}
346+
resultJSON = strings.ReplaceAll(resultJSON, "__DEVCONTAINER_USER_NAME__", devcontainerUserName)
347+
resultJSON = strings.ReplaceAll(resultJSON, "__DEVCONTAINER_HOME__", devcontainerHome)
308348

309349
ioutil.WriteFile(basePath, []byte(resultJSON), 0666)
310350

@@ -326,23 +366,27 @@ func loadJSONDocument(path string) (*dora_ast.RootNode, error) {
326366
return &baseDocument, nil
327367
}
328368

329-
func getDevcontainerName(devContainerJsonPath string) (string, error) {
369+
func getDevcontainerNameAndUserName(devContainerJsonPath string) (string, string) {
330370
// This doesn't use standard `json` pkg as devcontainer.json permits comments (and the default templates include them!)
331371

332372
buf, err := ioutil.ReadFile(devContainerJsonPath)
333373
if err != nil {
334-
return "", err
374+
return "", ""
335375
}
336376

337377
c, err := dora.NewFromBytes(buf)
338378
if err != nil {
339-
return "", err
379+
return "", ""
340380
}
341381

342382
name, err := c.GetString("$.name")
343383
if err != nil {
344-
return "", err
384+
name = ""
385+
}
386+
userName, err := c.GetString("$.remoteUser")
387+
if err != nil {
388+
userName = ""
345389
}
346390

347-
return name, nil
391+
return name, userName
348392
}

internal/pkg/devcontainers/snippet_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,65 @@ RUN /tmp/script.sh
426426
RUN echo hi2
427427
`, string(buf))
428428
}
429+
430+
func TestFolderAddSnippet_InsertsSnippetsInDockerfile(t *testing.T) {
431+
432+
root, err := ioutil.TempDir("", "devcontainer*")
433+
defer os.RemoveAll(root)
434+
435+
// set up snippet
436+
snippetFolder := filepath.Join(root, "snippets/test1")
437+
_ = os.MkdirAll(snippetFolder, 0755)
438+
snippetJSONFilename := filepath.Join(snippetFolder, "snippet.json")
439+
_ = ioutil.WriteFile(snippetJSONFilename, []byte(`{
440+
"actions": [
441+
{
442+
"type": "dockerfileSnippet",
443+
"content": "ENV FOO=BAR"
444+
},
445+
{
446+
"type": "dockerfileSnippet",
447+
"content": "# testing\nENV WIBBLE=BIBBLE"
448+
}
449+
]
450+
}`), 0755)
451+
452+
// set up devcontainer
453+
targetFolder := filepath.Join(root, "target")
454+
devcontainerFolder := filepath.Join(targetFolder, ".devcontainer")
455+
_ = os.MkdirAll(devcontainerFolder, 0755)
456+
457+
_ = ioutil.WriteFile(filepath.Join(devcontainerFolder, "Dockerfile"), []byte(`FROM foo
458+
RUN echo hi
459+
460+
# __DEVCONTAINER_SNIPPET_INSERT__
461+
462+
RUN echo hi2
463+
`), 0755)
464+
465+
// Add snippet
466+
snippet := DevcontainerSnippet{
467+
Name: "test",
468+
Path: snippetFolder,
469+
Type: DevcontainerSnippetTypeFolder,
470+
}
471+
err = addSnippetToDevcontainer(targetFolder, &snippet)
472+
if !assert.NoError(t, err) {
473+
return
474+
}
475+
476+
buf, err := ioutil.ReadFile(filepath.Join(devcontainerFolder, "Dockerfile"))
477+
assert.NoError(t, err)
478+
assert.Equal(t, `FROM foo
479+
RUN echo hi
480+
481+
ENV FOO=BAR
482+
483+
# testing
484+
ENV WIBBLE=BIBBLE
485+
486+
# __DEVCONTAINER_SNIPPET_INSERT__
487+
488+
RUN echo hi2
489+
`, string(buf))
490+
}

internal/pkg/devcontainers/template.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ func CopyTemplateToFolder(templatePath string, targetFolder string, devcontainer
137137
func SetDevcontainerName(devContainerJsonPath string, name string) error {
138138
// This doesn't use `json` as devcontainer.json permits comments (and the default templates include them!)
139139

140+
// TODO - update this to use dora to query
141+
// TODO - update this to replace __DEVCONTAINER_USER_NAME__ and __DEVCONTAINER_HOME__
142+
140143
buf, err := ioutil.ReadFile(devContainerJsonPath)
141144
if err != nil {
142145
return fmt.Errorf("error reading file %q: %s", devContainerJsonPath, err)

0 commit comments

Comments
 (0)