diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..cdd94f7f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,19 @@ +# Envbuilder Development Guide + +## Build/Test/Lint Commands +- Build: `make build` +- Run tests: `go test -count=1 ./...` +- Run tests with race detection: `go test -race -count=3 ./...` +- Run a single test: `go test -v -count=1 ./path/to/package -run TestName` +- Lint Go code: `make lint/go` +- Lint shell scripts: `make lint/shellcheck` + +## Code Style Guidelines +- Imports: Standard Go import grouping (stdlib, external, internal) +- Error handling: Return errors with context using `fmt.Errorf("operation: %w", err)` +- Logging: Use the provided logger (`options.Logger`) with appropriate log levels +- Testing: Use testify for assertions and test setup +- Types: Strong typing with proper interfaces, use Go 1.22+ features +- Naming: Follow Go conventions (camelCase for unexported, PascalCase for exported) +- Documentation: Write clear godoc comments for exported functions and types +- File organization: Keep related functionality in the same package \ No newline at end of file diff --git a/devcontainer/devcontainer_test.go b/devcontainer/devcontainer_test.go index d304e763..f7daf1da 100644 --- a/devcontainer/devcontainer_test.go +++ b/devcontainer/devcontainer_test.go @@ -43,6 +43,26 @@ func TestParse(t *testing.T) { require.Equal(t, "Dockerfile", parsed.Build.Dockerfile) } +func TestParseDirectTarball(t *testing.T) { + t.Parallel() + raw := `{ + "build": { + "dockerfile": "Dockerfile", + "context": ".", + }, + // Comments here! + "image": "codercom/code-server:latest", + "features": { + "https://github.com/coder/envbuilder/raw/94e6c14f5252ae47fcaf0886e6f3f7db91a75ce8/devcontainer/features/testdata/feature-tarball/devcontainer-feature-hello.tgz": { + "greeting": "Hello" + } + } +}` + parsed, err := devcontainer.Parse([]byte(raw)) + require.NoError(t, err) + require.Equal(t, "Dockerfile", parsed.Build.Dockerfile) +} + func TestCompileWithFeatures(t *testing.T) { t.Parallel() registry := registrytest.New(t) diff --git a/devcontainer/features/features_test.go b/devcontainer/features/features_test.go index 389193c6..5d170f17 100644 --- a/devcontainer/features/features_test.go +++ b/devcontainer/features/features_test.go @@ -51,6 +51,25 @@ func TestExtract(t *testing.T) { _, err := features.Extract(fs, "", "/", ref) require.ErrorContains(t, err, "id is required") }) + + t.Run("LoadTarballArtifacts", func(t *testing.T) { + t.Run("Success", func(t *testing.T) { + t.Parallel() + ref := "https://127.0.0.1:12345/devcontainer-feature-hello.tgz" + fs := memfs.New() + _, err := features.Extract(fs, "", "/", ref) + require.NoError(t, err) + }) + t.Run("InvalidName", func(t *testing.T) { + t.Parallel() + + ref := "https://127.0.0.1:12345/invalid-name-hello.tgz" + + fs := memfs.New() + _, err := features.Extract(fs, "", "/", ref) + require.ErrorContains(t, err, "invalid name") + }) + }) t.Run("Success", func(t *testing.T) { t.Parallel() registry := registrytest.New(t) @@ -117,4 +136,11 @@ func TestCompile(t *testing.T) { require.Equal(t, "FROM scratch AS envbuilder_feature_test\nCOPY --from=coder/test:latest / /", strings.TrimSpace(fromDirective)) require.Equal(t, "WORKDIR /.envbuilder/features/test\nRUN --mount=type=bind,from=envbuilder_feature_test,target=/.envbuilder/features/test,rw _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(runDirective)) }) + // t.Run("Tarball", func(t *testing.T) { + // t.Parallel() + // spec := &features.Spec{} + // _, directive, err := spec.Compile("https://127.0.0.1:12345/devcontainer-feature-hello.tgz", "hello", "/", "containerUser", "remoteUser", false, nil) + // require.NoError(t, err) + // require.Equal(t, "WORKDIR /\nRUN _CONTAINER_USER=\"containerUser\" _REMOTE_USER=\"remoteUser\" ./install.sh", strings.TrimSpace(directive)) + // }) } diff --git a/devcontainer/features/testdata/feature-tarball/devcontainer-feature-hello.tgz b/devcontainer/features/testdata/feature-tarball/devcontainer-feature-hello.tgz new file mode 100644 index 00000000..2ef02a6b Binary files /dev/null and b/devcontainer/features/testdata/feature-tarball/devcontainer-feature-hello.tgz differ diff --git a/integration/integration_test.go b/integration/integration_test.go index 913ab567..4b3dc86c 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -574,6 +574,31 @@ func TestBuildFromDevcontainerWithFeatures(t *testing.T) { require.Equal(t, "hello from test 3!", strings.TrimSpace(test3Output)) } +func TestDirectTarballFeaturePath(t *testing.T) { + t.Parallel() + feature3Spec, err := json.Marshal(features.Spec{ + ID: "test3", + Name: "test3", + Version: "1.0.0", + Options: map[string]features.Option{ + "grape": { + Type: "string", + }, + }, + } + spec := `{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "https://github.com/coder/envbuilder/raw/94e6c14f5252ae47fcaf0886e6f3f7db91a75ce8/devcontainer/features/testdata/feature-tarball/devcontainer-feature-hello.tgz": { + "greeting": "Hello" + } + } +}` +spec := &features.Spec{} +require.Equal(t, "/.envbuilder/feature/test-d8e8fc", spec.Compile()("test")) +} + + func TestBuildFromDockerfileAndConfig(t *testing.T) { t.Parallel()