Skip to content

Commit 99bc377

Browse files
committed
Added testing, added override concept
1 parent 55dc11b commit 99bc377

21 files changed

+817
-22
lines changed

.devcontainer/devcontainer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"features": {
88
"ghcr.io/devcontainers/features/go:1.3.2": {
99
"version": "1.24.5"
10-
}
10+
},
11+
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
12+
"ghcr.io/devcontainers-extra/features/devcontainers-cli:1": {}
1113
},
1214
"customizations": {
1315
"vscode": {

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1+
# Cache for go modules in dev container
12
.go_cache/
3+
4+
# Packaged features
5+
output/
6+
7+
# Cache for tests
8+
.scenario-test/

build/build.go

Lines changed: 241 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,28 @@
11
package main
22

33
import (
4+
"builder/installer"
5+
"encoding/json"
6+
"fmt"
47
"os"
8+
"os/exec"
9+
"path"
10+
"path/filepath"
511

612
"github.com/roemer/gotaskr"
13+
"github.com/roemer/gotaskr/execr"
14+
"github.com/roemer/gotaskr/gttools"
15+
"github.com/roemer/gotaskr/log"
716
)
817

18+
////////////////////////////////////////////////////////////
19+
// Variables
20+
////////////////////////////////////////////////////////////
21+
22+
var featureList = []string{
23+
"go",
24+
}
25+
926
////////////////////////////////////////////////////////////
1027
// Main
1128
////////////////////////////////////////////////////////////
@@ -19,7 +36,230 @@ func main() {
1936
////////////////////////////////////////////////////////////
2037

2138
func init() {
22-
gotaskr.Task("Build", func() error {
39+
gotaskr.Task("Update-Readme-Files", func() error {
40+
for _, feature := range featureList {
41+
if err := BuildReadmeForFeature(fmt.Sprintf("features/src/%s", feature)); err != nil {
42+
return err
43+
}
44+
}
2345
return nil
2446
})
47+
48+
gotaskr.Task("Feature:go:Package", func() error {
49+
return packageFeature("go")
50+
})
51+
gotaskr.Task("Feature:go:Test", func() error {
52+
return testFeature("go")
53+
})
54+
gotaskr.Task("Feature:go:Publish", func() error {
55+
return publishFeature("go")
56+
})
57+
}
58+
59+
////////////////////////////////////////////////////////////
60+
// Helpers
61+
////////////////////////////////////////////////////////////
62+
63+
func buildGo(workingDirectory string, binaryName string) error {
64+
// Check if a go installer exists and only compile it then
65+
if _, err := os.Stat(filepath.Join(workingDirectory, "installer.go")); err != nil {
66+
if os.IsNotExist(err) {
67+
log.Information("No go installer found, skip compiling")
68+
return nil
69+
} else {
70+
return err
71+
}
72+
}
73+
74+
// Force static linking
75+
os.Setenv("CGO_ENABLED", "0")
76+
// Compile the go installer
77+
cmd := exec.Command("go", "build", "-o", binaryName, "-ldflags", "-w", ".")
78+
cmd.Env = os.Environ()
79+
cmd.Env = append(cmd.Env, "GOOS=linux")
80+
cmd.Env = append(cmd.Env, "GOARCH=amd64")
81+
cmd.Dir = workingDirectory
82+
if err := execr.RunCommand(true, cmd); err != nil {
83+
return err
84+
}
85+
fullPath := filepath.Join(workingDirectory, binaryName)
86+
fi, err := os.Stat(fullPath)
87+
if err != nil {
88+
return err
89+
}
90+
log.Informationf("Built %s with a size of %s", fullPath, installer.HumanizeBytes(fi.Size(), false))
91+
return nil
92+
}
93+
94+
func packageFeature(featureName string) error {
95+
featurePath := path.Join("features/src", featureName)
96+
97+
// Build the installer
98+
if err := buildGo(featurePath, "installer"); err != nil {
99+
return err
100+
}
101+
defer os.Remove(filepath.Join(featurePath, "installer"))
102+
103+
// Package the feature
104+
settings := &gttools.DevContainerCliFeaturesPackageSettings{
105+
Target: featurePath,
106+
ForceCleanOutputFolder: gttools.True,
107+
}
108+
settings.OutputToConsole = true
109+
return gotaskr.Tools.DevContainerCli.FeaturesPackage(settings)
110+
}
111+
112+
func testFeature(featureName string) error {
113+
// Prepare the temporary folder for the devcontainer spec
114+
testPath := ".scenario-test"
115+
os.RemoveAll(testPath)
116+
os.MkdirAll(testPath, os.ModePerm)
117+
118+
// Read the images that should be used for testing
119+
testImagesFile := path.Join("features/test", featureName, "test-images.json")
120+
testImagesContent, err := os.ReadFile(testImagesFile)
121+
if err != nil {
122+
return err
123+
}
124+
var testImages []string
125+
if err := json.Unmarshal(testImagesContent, &testImages); err != nil {
126+
return err
127+
}
128+
129+
// Read and parse the scenario file
130+
scenariosFile := path.Join("features/test", featureName, "scenarios.json")
131+
fileContent, err := os.ReadFile(scenariosFile)
132+
if err != nil {
133+
return err
134+
}
135+
var jsonData map[string]json.RawMessage
136+
if err := json.Unmarshal(fileContent, &jsonData); err != nil {
137+
return err
138+
}
139+
// Loop thru the scenarios
140+
for scenarioName, scenarioContent := range jsonData {
141+
log.Informationf("Processing scenario '%s'", scenarioName)
142+
143+
// Loop thru the base images
144+
for _, testImage := range testImages {
145+
log.Informationf("Testing with image '%s'", testImage)
146+
147+
// Clear and prepare the devcontainer path
148+
devcontainerPath := path.Join(testPath, ".devcontainer")
149+
os.RemoveAll(devcontainerPath)
150+
os.MkdirAll(devcontainerPath, os.ModePerm)
151+
152+
// Write the devcontainer spec file
153+
devcontainerSpecPath := path.Join(devcontainerPath, "devcontainer.json")
154+
if err := os.WriteFile(devcontainerSpecPath, scenarioContent, os.ModePerm); err != nil {
155+
return err
156+
}
157+
158+
// Copy the verify-script
159+
data, err := os.ReadFile(path.Join("features/test", featureName, scenarioName+".sh"))
160+
if err != nil {
161+
return err
162+
}
163+
if err := os.WriteFile(path.Join(devcontainerPath, "check.sh"), data, os.ModePerm); err != nil {
164+
return err
165+
}
166+
167+
// Copy the functions.sh file
168+
data, err = os.ReadFile(path.Join("features/test/functions.sh"))
169+
if err != nil {
170+
return err
171+
}
172+
if err := os.WriteFile(path.Join(devcontainerPath, "functions.sh"), data, os.ModePerm); err != nil {
173+
return err
174+
}
175+
176+
// Write the Dockerfile
177+
if err := os.WriteFile(path.Join(devcontainerPath, "Dockerfile"), []byte(fmt.Sprintf(`
178+
FROM %s
179+
ADD check.sh /tmp/check.sh
180+
ADD functions.sh /tmp/functions.sh
181+
`, testImage)), os.ModePerm); err != nil {
182+
return err
183+
}
184+
185+
// Copy the required feature
186+
originalFeaturePath := path.Join("features/src", featureName)
187+
copiedFeaturePath := path.Join(devcontainerPath, featureName)
188+
if err := os.CopyFS(copiedFeaturePath, os.DirFS(originalFeaturePath)); err != nil {
189+
return err
190+
}
191+
192+
// Build the go installer inside the feature
193+
if err := buildGo(copiedFeaturePath, "installer"); err != nil {
194+
return err
195+
}
196+
197+
// Build the devcontainer
198+
imageName := fmt.Sprintf("dev-container-feature-%s-scenario-%s-test", featureName, scenarioName)
199+
if err := gotaskr.Tools.DevContainerCli.Build(&gttools.DevContainerCliBuildSettings{
200+
ToolSettingsBase: gttools.ToolSettingsBase{OutputToConsole: true},
201+
WorkspaceFolder: testPath,
202+
ImageNames: []string{imageName},
203+
}); err != nil {
204+
return err
205+
}
206+
defer execr.Run(false, "docker", "image", "rm", imageName)
207+
208+
// Run the check in the container
209+
checkError := execr.Run(true, "docker", "run", "-t", "--rm", "-v", "/var/run/docker.sock:/var/run/docker.sock", imageName, "sh", "-c", "/tmp/check.sh")
210+
if checkError != nil {
211+
return fmt.Errorf("check failed: %w", checkError)
212+
}
213+
fmt.Println("Check was successfull")
214+
}
215+
}
216+
217+
return nil
218+
219+
// TODO: Somewhen in the future this can be done with the devcontainer cli
220+
/*featurePath := path.Join("features/src", featureName)
221+
// Build the installer
222+
if err := buildGo(featurePath, "installer"); err != nil {
223+
return err
224+
}
225+
defer os.Remove(filepath.Join(featurePath, "installer"))
226+
227+
if err := gotaskr.Tools.DevContainerCli.FeaturesTest(&gttools.DevContainerCliFeaturesTestSettings{
228+
ToolSettingsBase: gttools.ToolSettingsBase{OutputToConsole: true},
229+
ProjectFolder: "./features",
230+
Features: []string{featureName},
231+
LogLevel: gttools.DEV_CONTAINER_CLI_LOG_LEVEL_DEBUG,
232+
SkipAutogenerated: gttools.True,
233+
SkipDuplicated: gttools.True,
234+
}); err != nil {
235+
return err
236+
}*/
237+
}
238+
239+
func publishFeature(featureName string) error {
240+
// TODO
241+
return nil
242+
243+
/*
244+
featurePath := path.Join("features/src", featureName)
245+
246+
// Build the installer
247+
if err := buildGo(featurePath, "installer"); err != nil {
248+
return err
249+
}
250+
defer os.Remove(filepath.Join(featurePath, "installer"))
251+
252+
// Set OCI authentication
253+
if err := setOCIAuth(); err != nil {
254+
return err
255+
}
256+
// Build and publish the feature
257+
settings := &gttools.DevContainerCliFeaturesPublishSettings{
258+
Target: featurePath,
259+
Registry: registry,
260+
Namespace: namespace,
261+
}
262+
settings.OutputToConsole = true
263+
return gotaskr.Tools.DevContainerCli.FeaturesPublish(settings)
264+
*/
25265
}

build/json.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"os"
7+
"path/filepath"
8+
"sort"
9+
)
10+
11+
// Reads and parses the "devcontainer-feature.json" file.
12+
func ParseFeatureJson(featurePath string) (*FeatureSpec, error) {
13+
fileContent, err := os.ReadFile(filepath.Join(featurePath, "devcontainer-feature.json"))
14+
if err != nil {
15+
return nil, err
16+
}
17+
var jsonData *FeatureSpec
18+
if err := json.Unmarshal(fileContent, &jsonData); err != nil {
19+
return nil, err
20+
}
21+
return jsonData, nil
22+
}
23+
24+
type FeatureSpec struct {
25+
Id string `json:"id"`
26+
Version string `json:"version"`
27+
Name string `json:"name"`
28+
Description string `json:"description"`
29+
Options OrderedOptionsMap `json:"options"`
30+
Customizations FeatureCustomizations `json:"customizations"`
31+
}
32+
33+
type FeatureOption struct {
34+
Type string `json:"type"`
35+
Default any `json:"default"`
36+
Description string `json:"description"`
37+
Proposals []string `json:"proposals"`
38+
}
39+
40+
type FeatureCustomizations struct {
41+
VsCode FeatureCustomizationsVsCode `json:"vscode"`
42+
}
43+
44+
type FeatureCustomizationsVsCode struct {
45+
Extensions []string `json:"extensions"`
46+
}
47+
48+
type OrderedOptionsMap struct {
49+
Order []string
50+
Map map[string]FeatureOption
51+
}
52+
53+
// Custom unmarshaller that also keeps the order of keys in a slice.
54+
func (om *OrderedOptionsMap) UnmarshalJSON(b []byte) error {
55+
json.Unmarshal(b, &om.Map)
56+
57+
index := make(map[string]int)
58+
for key := range om.Map {
59+
om.Order = append(om.Order, key)
60+
esc, _ := json.Marshal(key) //Escape the key
61+
index[key] = bytes.Index(b, esc)
62+
}
63+
64+
sort.Slice(om.Order, func(i, j int) bool { return index[om.Order[i]] < index[om.Order[j]] })
65+
return nil
66+
}

0 commit comments

Comments
 (0)