Skip to content

feat(devcontainers): implement features package for OCI and local resolution#319

Merged
feloy merged 7 commits intoopenkaiden:mainfrom
feloy:features-pkg
Apr 23, 2026
Merged

feat(devcontainers): implement features package for OCI and local resolution#319
feloy merged 7 commits intoopenkaiden:mainfrom
feloy:features-pkg

Conversation

@feloy
Copy link
Copy Markdown
Contributor

@feloy feloy commented Apr 22, 2026

Implements pkg/devcontainers/features as specified in issue #302:

  • Feature, FeatureMetadata, and FeatureOptions interfaces (all unexported impls)
  • OCI registry download: anonymous Bearer auth, manifest fetch, gzip/plain tar extraction, path-traversal sanitisation
  • Local file tree download for ./… and ../… IDs
  • FromMap classifies IDs and returns a deterministically sorted slice
  • Order runs Kahn's topological sort on installsAfter; matches versionless installsAfter values against versioned feature IDs by stripping the tag
  • Unit tests covering all acceptance criteria; integration tests against the real ghcr.io registry (build tag: integration)
  • /working-with-devcontainers skill with spec coverage table
  • Extend make test-integration target to ./... to include the new tests

Closes #302

@serbangeorge-m I have added a few integration tests and modified the make test-integration command to cover them. Please check I didn't break anything

Can be tested on PR #323 which includes this PR

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 82.72251% with 66 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/devcontainers/features/oci.go 74.88% 29 Missing and 25 partials ⚠️
pkg/devcontainers/features/local.go 80.00% 4 Missing and 4 partials ⚠️
pkg/devcontainers/features/features.go 93.75% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@feloy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 12 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 53 minutes and 12 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 041d6043-1d2b-4c1d-8bb0-18649132fa87

📥 Commits

Reviewing files that changed from the base of the PR and between de8f5a9 and ed6e043.

📒 Files selected for processing (12)
  • .agents/skills/working-with-devcontainers/SKILL.md
  • AGENTS.md
  • Makefile
  • pkg/devcontainers/features/features.go
  • pkg/devcontainers/features/features_test.go
  • pkg/devcontainers/features/local.go
  • pkg/devcontainers/features/local_test.go
  • pkg/devcontainers/features/local_unix_test.go
  • pkg/devcontainers/features/metadata.go
  • pkg/devcontainers/features/oci.go
  • pkg/devcontainers/features/oci_integration_test.go
  • pkg/devcontainers/features/oci_test.go
📝 Walkthrough

Walkthrough

Adds a new package pkg/devcontainers/features that models Dev Container Features with Feature interfaces, local and OCI implementations, metadata parsing and option merging, deterministic pre-ordering and dependency ordering, plus comprehensive unit and integration tests and documentation. No changes to existing public APIs outside the new package.

Changes

Cohort / File(s) Summary
Documentation
.agents/skills/working-with-devcontainers/SKILL.md, AGENTS.md
New guide and AGENTS section documenting package API shapes, four-phase workflow (FromMap → Download → Order → Merge), ID classification rules, option normalization/validation, OCI/local behaviors, and testing guidance.
Build / CI
Makefile
test-integration target broadened to run go test ./... instead of ./pkg/cmd/, expanding integration test package scope.
Core interfaces & orchestration
pkg/devcontainers/features/features.go
New package with exported interfaces Feature, FeatureMetadata, FeatureOptions, plus FromMap (ID classification, deterministic ID-sorted slice, https/tgz rejection) and Order (builds dependency graph from InstallsAfter, version-stripped ID matching, Kahn topological sort, cycle detection).
Metadata & option handling
pkg/devcontainers/features/metadata.go
Parsing of devcontainer-feature.json, concrete FeatureMetadata and FeatureOptions implementations, FeatureOptions.Merge with key normalization (UPPER + non-alnum→_), defaults, boolean/string coercion, enum/type validation, and parseMetadata(dir) entrypoint.
OCI feature implementation
pkg/devcontainers/features/oci.go
ociFeature with OCI ref parsing (default registry/tag), manifest fetch with Bearer auth challenge handling, layer blob download with digest verification, gzip auto-detect, tar extraction with path-traversal and symlink/hardlink rejection, and NewOCIFeatureWithClient.
Local feature implementation
pkg/devcontainers/features/local.go
localFeature that resolves workspace-relative paths, recursively copies directory into dest (rejects symlinks and non-regular entries), and returns parsed metadata.
Tests (unit & integration)
pkg/devcontainers/features/*_test.go, pkg/devcontainers/features/oci_integration_test.go
Extensive tests covering option merging, FromMap behavior, Order (ordering, cycles, missing metadata), OCI token/manifest/layer flows, tar extraction edge cases, local copy edge cases, httptest-based OCI integration, and integration tests guarded by build tag. Large test additions may affect test runtime and CI.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant FromMap
    participant Feature as Feature (local / OCI)
    participant OCI as OCI Registry
    participant FS as FileSystem
    participant Meta as Metadata Parser
    participant Order

    Client->>FromMap: FromMap(featuresMap, workspaceDir)
    FromMap-->>Client: []Feature, userOptions

    loop parallel per feature
        Client->>Feature: Download(ctx, destDir)
        alt local feature
            Feature->>FS: Resolve & copy workspace path -> destDir
        else OCI feature
            Feature->>OCI: Parse ref, fetch manifest (401→auth challenge)
            OCI-->>Feature: Bearer token (if needed)
            Feature->>OCI: Download layer blobs
            Feature->>FS: Extract tar/gzip -> destDir (sanitize paths, reject symlinks)
        end
        FS->>Meta: Read devcontainer-feature.json
        Meta-->>Feature: FeatureMetadata
        Feature-->>Client: FeatureMetadata
    end

    Client->>Order: Order(feats, metadata)
    Order->>Order: Build graph from InstallsAfter (strip versions)
    Order->>Order: Kahn topological sort (stable)
    Order-->>Client: ordered []Feature

    loop for ordered features
        Client->>Meta: metadata[feat.ID()].Options().Merge(userOptions)
        Meta-->>Client: normalized options map
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main implementation of the features package for OCI and local resolution as specified in issue #302.
Description check ✅ Passed The description is directly related to the changeset, detailing implementation of the features package per issue #302 with OCI/local support and related test/doc additions.
Linked Issues check ✅ Passed The PR implements all key objectives from issue #302: Feature/FeatureMetadata/FeatureOptions interfaces, FromMap ID classification and sorting, Order topological sort with versionless matching, OCI Bearer auth/manifest/extraction, local file copying, FeatureOptions.Merge validation, and comprehensive unit/integration tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #302: new features package, supporting OCI/local implementations, tests, documentation (SKILL.md, AGENTS.md), and Makefile extension for integration tests. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.agents/skills/working-with-devcontainers/SKILL.md:
- Around line 87-90: The fenced code block showing mappings for "install-tools"
→ "INSTALL_TOOLS" and "go.version" → "GO_VERSION" is missing a language tag;
update that fenced block to include the text language tag (use ```text) so the
block becomes a ```text fenced block containing the two lines referencing
"install-tools" / INSTALL_TOOLS and "go.version" / GO_VERSION, satisfying the
markdown guideline that all fenced code blocks include a language tag.

In `@pkg/devcontainers/features/features_test.go`:
- Around line 397-413: The test TestOrder_InstallsAfterExternalFeatureIgnored
assumes that an installsAfter reference to a feature not present in the input
list is ignored, but the spec/PR requires missing dependency references to cause
an error; update the behavior and test accordingly: modify the implementation of
features.Order to validate each FeatureMetadata.installsAfter entry (and
similarly installsBefore if present) and return an error when any referenced
feature ID is not present in the provided feats list, and then change
TestOrder_InstallsAfterExternalFeatureIgnored to expect an error from
features.Order (use the fakeMetadata.installsAfter reference as-is) rather than
a successful ordering; ensure the error message clearly identifies the missing
dependency and surface it from Order so the test can assert non-nil err.

In `@pkg/devcontainers/features/features.go`:
- Around line 76-77: The error branch that currently uses strings.HasPrefix(id,
"http://") || strings.HasPrefix(id, "https://") mislabels any HTTP(S) input as a
"Tgz URI"; update the check and message: change the condition to only treat
actual .tgz archives (e.g., strings.HasSuffix(id, ".tgz")) and/or revise the
fmt.Errorf call so it says that direct HTTP(S) feature URIs are not supported
(reference the id variable, the strings.HasPrefix/HasSuffix check, and the
fmt.Errorf call in that case branch). Ensure the branch behavior matches the new
check so plain HTTP URLs get the correct message and true .tgz URIs are handled
appropriately.

In `@pkg/devcontainers/features/local.go`:
- Around line 54-89: The copy logic currently follows symlinks (WalkDir treats
them as non-directories and copyFile opens targets), allowing files outside the
feature tree to be copied; update copyDir and copyFile to explicitly detect and
skip symlinks: inside copyDir's WalkDirFunc check d.Type()&fs.ModeSymlink (or
call os.Lstat on path) and return nil to skip symlink entries, and in copyFile
re-check using os.Lstat(path) to refuse/skip if the source is a symlink before
calling os.Open; keep creating directories and copying regular files unchanged.

In `@pkg/devcontainers/features/metadata.go`:
- Around line 74-128: Defaults are currently written into result with
fmt.Sprintf and skip the type/enum checks; change the initial "Apply defaults
first" loop to validate and normalize spec.Default using the same logic as the
userOptions handling (use normalizeKey, o.specs, spec.Default, result) — i.e.,
for spec.Type == "boolean" accept bool or string "true"/"false" and write
"true"/"false", for "string" ensure the default is a string and validate against
spec.Enum if present, and return an error if the default is invalid; factor or
reuse the switch used for userOptions to avoid duplication so invalid defaults
fail early instead of being silently formatted.

In `@pkg/devcontainers/features/oci.go`:
- Around line 316-346: The extraction currently trusts tar.TypeSymlink entries
and later file writes can follow those symlinks (hdr.Name, target, os.Symlink,
os.OpenFile), allowing escapes from destDir; reject or validate symlinks by: for
tar.TypeSymlink, disallow absolute Linkname or compute resolved :=
filepath.Clean(filepath.Join(filepath.Dir(target), hdr.Linkname)) and ensure
strings.HasPrefix(resolved, filepath.Clean(destDir)+string(os.PathSeparator));
if it fails, return an error instead of creating the symlink; additionally,
before writing regular files (tar.TypeReg) call os.Lstat on target (and/or each
path component) and reject if any component is a symlink to prevent following
filesystem symlinks when creating the file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 388187a1-5abf-4bc9-bb2e-46d1b7278204

📥 Commits

Reviewing files that changed from the base of the PR and between 3173a4f and be373f6.

📒 Files selected for processing (9)
  • .agents/skills/working-with-devcontainers/SKILL.md
  • AGENTS.md
  • Makefile
  • pkg/devcontainers/features/features.go
  • pkg/devcontainers/features/features_test.go
  • pkg/devcontainers/features/local.go
  • pkg/devcontainers/features/metadata.go
  • pkg/devcontainers/features/oci.go
  • pkg/devcontainers/features/oci_integration_test.go

Comment thread .agents/skills/working-with-devcontainers/SKILL.md Outdated
Comment thread pkg/devcontainers/features/features_test.go
Comment thread pkg/devcontainers/features/features.go Outdated
Comment thread pkg/devcontainers/features/local.go
Comment thread pkg/devcontainers/features/metadata.go
Comment thread pkg/devcontainers/features/oci.go Outdated
Copy link
Copy Markdown
Contributor

@serbangeorge-m serbangeorge-m left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changes related to integration tests look ok

@feloy feloy marked this pull request as draft April 22, 2026 10:09
feloy added a commit to feloy/kdn that referenced this pull request Apr 22, 2026
…iden#319

- Add text language tag to fenced code block in SKILL.md
- Clarify HTTP(S) feature source error message in FromMap
- Update Order docstring: installsAfter soft-ignore is intentional per spec
- Reject symlinks and non-regular files in local feature copyDir
- Validate defaults with same type/enum rules as user options in Merge
- Replace symlink creation with error in extractTarEntries; add
  safeTarTarget helper for robust path-containment checks
- Update oci_test.go to expect error for symlink tar entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
@feloy feloy marked this pull request as ready for review April 22, 2026 12:13
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
pkg/devcontainers/features/features_test.go (1)

604-625: ⚠️ Potential issue | 🟠 Major

Align the missing-dependency behavior with the PR objective.

This test codifies ignoring an installsAfter reference that is not in feats, but the linked objectives say Order should error on missing references. Either update Order and this test to expect an error, or update the objective/spec docs if soft-ignore is intentional.

Proposed test expectation if missing references must error
-func TestOrder_InstallsAfterExternalFeatureIgnored(t *testing.T) {
+func TestOrder_MissingInstallsAfterDependencyReturnsError(t *testing.T) {
 	t.Parallel()
 
-	// Per the Dev Container spec, installsAfter is a soft ordering hint.
-	// References to features not present in feats (e.g. features from a
-	// different layer or not selected by the user) must be silently ignored,
-	// not treated as errors. This is distinct from a missing metadata entry
-	// (which is always an error because it means a feature in feats was not
-	// downloaded before Order was called).
 	feats := []features.Feature{&fakeFeature{id: "A"}}
 	metadata := map[string]features.FeatureMetadata{
 		"A": &fakeMetadata{installsAfter: []string{"external-feature"}},
 	}
 
-	ordered, err := features.Order(feats, metadata)
-	if err != nil {
-		t.Fatalf("Order: %v", err)
-	}
-	if len(ordered) != 1 || ordered[0].ID() != "A" {
-		t.Errorf("ordered = %v, want [A]", idsOf(ordered))
+	_, err := features.Order(feats, metadata)
+	if err == nil {
+		t.Fatal("expected error for missing installsAfter dependency, got nil")
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainers/features/features_test.go` around lines 604 - 625, The test
TestOrder_InstallsAfterExternalFeatureIgnored currently expects Order to
silently ignore an installsAfter reference not present in feats, but the PR
objective states features.Order should error on missing references; update the
test to assert that features.Order returns a non-nil error for this case (and
optionally that the error message mentions the missing dependency like
"external-feature") instead of succeeding, using the existing feats slice and
metadata (fakeFeature / fakeMetadata) to trigger the missing-reference path;
ensure the test checks err != nil and fails if Order returns nil so it aligns
with the intended behavior of features.Order.
pkg/devcontainers/features/local.go (1)

83-97: ⚠️ Potential issue | 🟠 Major

Re-check the source before opening and handle file close errors.

WalkDir rejects symlink entries, but copyFile still opens src without an Lstat re-check, and both deferred closes are flagged by errcheck. Re-check regular-file status immediately before opening and propagate write-side close errors.

Proposed fix
-func copyFile(src, dst string) error {
+func copyFile(src, dst string) (err error) {
+	info, err := os.Lstat(src)
+	if err != nil {
+		return err
+	}
+	if info.Mode()&os.ModeSymlink != 0 {
+		return fmt.Errorf("symlinks are not supported in local features: %s", src)
+	}
+	if !info.Mode().IsRegular() {
+		return fmt.Errorf("unsupported non-regular file in local feature: %s", src)
+	}
+
 	in, err := os.Open(src)
 	if err != nil {
 		return err
 	}
-	defer in.Close()
+	defer func() {
+		if closeErr := in.Close(); err == nil && closeErr != nil {
+			err = closeErr
+		}
+	}()
 
 	out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
 	if err != nil {
 		return err
 	}
-	defer out.Close()
+	defer func() {
+		if closeErr := out.Close(); err == nil && closeErr != nil {
+			err = closeErr
+		}
+	}()
 
 	_, err = io.Copy(out, in)
 	return err
 }

Run the lint target to confirm errcheck is clean:

#!/bin/bash
# Description: Verify static checks after handling Close errors.
make vet
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainers/features/local.go` around lines 83 - 97, In copyFile,
re-check the source file with os.Lstat(src) and ensure it's a regular file
before opening (so symlinks/rejected WalkDir entries are caught) and update the
function to capture and propagate Close errors instead of ignoring them: replace
simple defers for in.Close() and out.Close() with deferred closures that check
the returned error (e.g., if cerr := in.Close(); err == nil { err = cerr }) and
do the same for out.Close() (making sure you preserve and return any earlier
copy error but prefer to return a close error if it occurs), keeping the
io.Copy(out, in) logic and variable names (src, dst, in, out) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/devcontainers/features/local.go`:
- Around line 40-42: The Download method in localFeature builds srcDir with
filepath.Join which can remain relative; update localFeature.Download to convert
the joined/cleaned path to an absolute host path by calling filepath.Abs on
srcDir (after computing srcDir := filepath.Clean(filepath.Join(f.dir,
filepath.FromSlash(f.id)))); handle the error from filepath.Abs and return it if
non-nil, then use the absolute path for the subsequent copy/reading operations
so the feature source cannot change based on cwd (referencing
localFeature.Download, srcDir, and filepath.Abs).

In `@pkg/devcontainers/features/oci_integration_test.go`:
- Around line 53-54: Tests currently call feats[i].Download with
context.Background(), which can hang; replace those calls by creating a bounded
context via context.WithTimeout (e.g., 30s) and pass that ctx into every
Download call (for the occurrences around Download in oci_integration_test.go at
the spots using feats[0].Download and the other mentioned locations), remember
to call cancel() via defer immediately after creating the context to avoid
leaks, and propagate the new ctx into all Download invocations in the test.

In `@pkg/devcontainers/features/oci.go`:
- Around line 250-272: In downloadAndExtractLayer, compute and verify the
returned blob's digest (expected "sha256:<hex>" from the digest param) before
extracting: stream the response into a SHA-256 hasher while writing to a
temporary file or buffering (e.g., use io.TeeReader or write to tmp file),
compare hex(sum) prefixed with "sha256:" to the digest parameter, return an
error on mismatch, and only call extractTar (or a modified extractTar that reads
from the verified temp file) after verification; update downloadAndExtractLayer
and extractTar usage accordingly to ensure extraction happens from a verified
blob and avoid extracting on hash failure.
- Around line 141-162: The code currently calls resp.Body.Close(), gzip reader
Close, and file Close without handling returned errors which trips errcheck;
update all such calls: for response bodies (resp) you can explicitly discard
errors with `_ = resp.Body.Close()` where safe (e.g., after handling the error
branch in the manifest fetch block around the `resp` variable and similar blocks
at the other resp closes), but for resources where buffered writes may fail (the
gzip reader variable, e.g., `gz`, and file handles used for writing, e.g.,
`dstFile`/`f`) must check the Close() error and propagate it (return wrapped
fmt.Errorf or `err` up) — update Close usage in the functions that perform gzip
extraction and file writes (the gzip reader close near the creation of the gzip
reader and the file Close calls after writing) to capture and handle errors
instead of ignoring them.

---

Duplicate comments:
In `@pkg/devcontainers/features/features_test.go`:
- Around line 604-625: The test TestOrder_InstallsAfterExternalFeatureIgnored
currently expects Order to silently ignore an installsAfter reference not
present in feats, but the PR objective states features.Order should error on
missing references; update the test to assert that features.Order returns a
non-nil error for this case (and optionally that the error message mentions the
missing dependency like "external-feature") instead of succeeding, using the
existing feats slice and metadata (fakeFeature / fakeMetadata) to trigger the
missing-reference path; ensure the test checks err != nil and fails if Order
returns nil so it aligns with the intended behavior of features.Order.

In `@pkg/devcontainers/features/local.go`:
- Around line 83-97: In copyFile, re-check the source file with os.Lstat(src)
and ensure it's a regular file before opening (so symlinks/rejected WalkDir
entries are caught) and update the function to capture and propagate Close
errors instead of ignoring them: replace simple defers for in.Close() and
out.Close() with deferred closures that check the returned error (e.g., if cerr
:= in.Close(); err == nil { err = cerr }) and do the same for out.Close()
(making sure you preserve and return any earlier copy error but prefer to return
a close error if it occurs), keeping the io.Copy(out, in) logic and variable
names (src, dst, in, out) intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6fefdb36-be9e-441a-a5e4-a452b94e1c43

📥 Commits

Reviewing files that changed from the base of the PR and between be373f6 and b14ab0f.

📒 Files selected for processing (10)
  • .agents/skills/working-with-devcontainers/SKILL.md
  • AGENTS.md
  • Makefile
  • pkg/devcontainers/features/features.go
  • pkg/devcontainers/features/features_test.go
  • pkg/devcontainers/features/local.go
  • pkg/devcontainers/features/metadata.go
  • pkg/devcontainers/features/oci.go
  • pkg/devcontainers/features/oci_integration_test.go
  • pkg/devcontainers/features/oci_test.go
✅ Files skipped from review due to trivial changes (1)
  • .agents/skills/working-with-devcontainers/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • Makefile
  • pkg/devcontainers/features/features.go

Comment thread pkg/devcontainers/features/local.go
Comment thread pkg/devcontainers/features/oci_integration_test.go Outdated
Comment thread pkg/devcontainers/features/oci.go Outdated
Comment thread pkg/devcontainers/features/oci.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (1)
pkg/devcontainers/features/features_test.go (1)

606-627: ⚠️ Potential issue | 🟠 Major

Align this test with the missing-reference requirement.

The PR objective says Order should error on missing references, but this test locks in silently ignoring an installsAfter target that is absent from feats. If soft-ignore is intentional, update the objective/spec; otherwise expect an error here and update Order accordingly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/devcontainers/features/features_test.go` around lines 606 - 627, The test
TestOrder_InstallsAfterExternalFeatureIgnored currently asserts that
features.Order silently ignores an installsAfter reference to "external-feature"
even when that feature is not present in feats; update the test to reflect the
PR objective that Order should error on missing references by calling
features.Order(feats, metadata) and asserting that err is non-nil (and
optionally that the error message mentions the missing installsAfter target or
missing metadata), and adjust assertions so ordered is not considered
successful; keep references to the test name, the metadata map using
fakeMetadata with installsAfter, and the features.Order function to locate the
change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/devcontainers/features/features_test.go`:
- Around line 288-295: The test checks error contents after calling
meta.Options().Merge but uses t.Error before accessing err.Error(), which can
lead to a nil-panic; change those failing-path assertions to call t.Fatal (or
t.Fatalf) immediately when err == nil so the test stops before any err.Error()
usage—update occurrences around meta.Options().Merge(...) where the pattern is
`if err == nil { t.Error(...) } if !strings.Contains(err.Error(), "...") { ...
}` to instead `if err == nil { t.Fatal(...) }` (and use t.Fatalf when formatting
the message) for the blocks at the shown location and the other similar blocks
(around lines noted in the review).
- Around line 676-689: The test currently ignores errors from tar/gzip Close and
response writes which can hide failures; update the test to check and handle
errors returned by tw.Close(), tw2.Close(), gw.Close() and any w.Write(...) used
by the HTTP handler (or explicitly use io.Copy to io.Discard and check its
error) so failures surface and the produced layer bytes are valid; locate the
creation/closing of tw, tw2 and gw in features_test.go and the HTTP handler
write path and add error checks (fail the test with t.Fatalf or t.Fatal on
non-nil errors) after each Close and after writing the response body.

In `@pkg/devcontainers/features/local_test.go`:
- Around line 46-51: The test currently calls t.Error("expected error...") then
proceeds to use err.Error(), which will panic if err is nil; change the failing
assertions to t.Fatal (e.g., replace t.Error("expected error for non-existent
source directory, got nil") with t.Fatal(...)) so the test stops immediately
when err is nil before any err.Error() call, and make the same change for the
second occurrence around the block that starts at the other check (lines
referenced by the second occurrence around feats[0].Download and the subsequent
err.Error() usage).
- Around line 93-95: The test currently hardcodes "/etc/hosts" when creating a
symlink (os.Symlink(..., filepath.Join(featureDir, "z-link"))), which breaks
cross-platform runs; instead create a real temporary target inside the test's
temp workspace (use t.TempDir() or the existing workspaceDir), join its name
with filepath.Join to produce the target path, and call os.Symlink(targetPath,
filepath.Join(featureDir, "z-link")); ensure you use filepath.Join for the host
filesystem path and avoid any literal "/etc/hosts" strings so the test is
cross-platform and self-contained.

In `@pkg/devcontainers/features/local_unix_test.go`:
- Around line 62-67: Test currently calls feats[0].Download and uses t.Error on
nil err, which allows execution to continue and then calls err.Error() causing a
panic; change the failure at the check for nil error to t.Fatal so the test
stops immediately when Download unexpectedly succeeds, i.e., replace the
t.Error("expected error...") with t.Fatal("expected error...") in the test that
calls feats[0].Download to avoid calling err.Error() when err is nil.

In `@pkg/devcontainers/features/local.go`:
- Around line 86-100: The copyFile function currently defers in.Close() and
out.Close(), which hides close errors (especially delayed write errors from
out.Close()); change it to explicitly close files after io.Copy and propagate
any close errors: after calling io.Copy(out, in) capture its error into err,
then call closeErr := out.Close() and if err == nil set err = closeErr, then
call inCloseErr := in.Close() and if err == nil set err = inCloseErr; return
err. Use the existing symbols copyFile, in, out and io.Copy to locate and update
the cleanup so close failures are not ignored.

In `@pkg/devcontainers/features/oci_test.go`:
- Around line 574-579: The test calls extractTar and then uses t.Error before
inspecting err.Error(), which can dereference nil; change the failure handling
to fail fast by using t.Fatal (or t.Fatalf) when err is nil, e.g., in the test
around the call to extractTar(...) and the subsequent checks replace
t.Error("expected error...") with a t.Fatal or t.Fatalf so the test stops if err
is nil, then safely proceed to assert the error string contains "creating gzip
reader" from err.Error(); reference the extractTar call and the err variable in
oci_test.go when making this change.
- Around line 189-192: The httptest handler's response writes (e.g.,
fmt.Fprint/w.Write/io.WriteString in the srv handler closure) should explicitly
discard errors when the write failure is non-actionable (use _ = ...) and all
tar/gzip finalizing Close calls (e.g., calls on tar.NewWriter.Close and
gzip.NewWriter.Close in the tests) must have their returned errors checked and
handled (fail the test with t.Fatalf or t.Errorf) so the archive bytes are
guaranteed finalized; update each occurrence (handler writes near the srv
handler and every tar.Writer/gzip.Writer Close call) to either explicitly ignore
the error or check it and fail the test accordingly.

In `@pkg/devcontainers/features/oci.go`:
- Around line 280-286: The defer currently calls os.Remove(tmpPath) and ignores
its error which fails errcheck; update the cleanup to explicitly discard the
error so linters are satisfied — e.g., replace the plain defer
os.Remove(tmpPath) with an explicit discard wrapper (referencing tmpPath and
os.Remove) such as a deferred anonymous function that calls os.Remove(tmpPath)
and assigns its result to the blank identifier to explicitly ignore the error.

---

Duplicate comments:
In `@pkg/devcontainers/features/features_test.go`:
- Around line 606-627: The test TestOrder_InstallsAfterExternalFeatureIgnored
currently asserts that features.Order silently ignores an installsAfter
reference to "external-feature" even when that feature is not present in feats;
update the test to reflect the PR objective that Order should error on missing
references by calling features.Order(feats, metadata) and asserting that err is
non-nil (and optionally that the error message mentions the missing
installsAfter target or missing metadata), and adjust assertions so ordered is
not considered successful; keep references to the test name, the metadata map
using fakeMetadata with installsAfter, and the features.Order function to locate
the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0363132d-d1fe-4bbe-ac6c-9f379e7bd08b

📥 Commits

Reviewing files that changed from the base of the PR and between b14ab0f and de8f5a9.

📒 Files selected for processing (6)
  • pkg/devcontainers/features/features_test.go
  • pkg/devcontainers/features/local.go
  • pkg/devcontainers/features/local_test.go
  • pkg/devcontainers/features/local_unix_test.go
  • pkg/devcontainers/features/oci.go
  • pkg/devcontainers/features/oci_test.go

Comment thread pkg/devcontainers/features/features_test.go
Comment thread pkg/devcontainers/features/features_test.go Outdated
Comment thread pkg/devcontainers/features/local_test.go
Comment thread pkg/devcontainers/features/local_test.go Outdated
Comment thread pkg/devcontainers/features/local_unix_test.go
Comment thread pkg/devcontainers/features/local.go Outdated
Comment thread pkg/devcontainers/features/oci_test.go
Comment thread pkg/devcontainers/features/oci_test.go
Comment thread pkg/devcontainers/features/oci.go Outdated
@feloy feloy force-pushed the features-pkg branch 2 times, most recently from 9394774 to 7de9916 Compare April 23, 2026 11:56
feloy added a commit to feloy/kdn that referenced this pull request Apr 23, 2026
…iden#319

Production fixes:
- copyFile uses named return to propagate Close errors (errcheck +
  delayed write errors from out.Close() now surface correctly)
- defer os.Remove(tmpPath) explicitly discards error to satisfy errcheck

Test fixes across features_test.go, oci_test.go, oci_integration_test.go,
local_test.go, and local_unix_test.go:
- t.Error → t.Fatal before err.Error() calls (prevent nil-panic)
- tw/gw Close calls checked with t.Fatalf (archive bytes must be valid)
- Handler fmt.Fprint/w.Write explicitly discarded with _, _ = (errcheck)
- Symlink test target replaced from /etc/hosts with a temp file (cross-platform)
- Integration tests use context.WithTimeout(2m) instead of context.Background()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
feloy and others added 7 commits April 23, 2026 18:26
…olution

Implements pkg/devcontainers/features as specified in issue openkaiden#302:
- Feature, FeatureMetadata, and FeatureOptions interfaces (all unexported impls)
- OCI registry download: anonymous Bearer auth, manifest fetch, gzip/plain tar
  extraction, path-traversal sanitisation
- Local file tree download for ./… and ../… IDs
- FromMap classifies IDs and returns a deterministically sorted slice
- Order runs Kahn's topological sort on installsAfter; matches versionless
  installsAfter values against versioned feature IDs by stripping the tag
- Unit tests covering all acceptance criteria; integration tests against the
  real ghcr.io registry (build tag: integration)
- /working-with-devcontainers skill with spec coverage table
- Extend make test-integration target to ./... to include the new tests

Closes openkaiden#302

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
Add targeted tests to cover previously untested branches in metadata.go
and oci.go, raising overall package coverage from 61.6% to 90.4%.

- metadata.go: InstallsAfter(), all Merge() branches (unknown option,
  bool user option true/false, bool string coercion, invalid bool
  string, wrong type for boolean, unsupported option type),
  parseMetadata() invalid JSON
- oci.go: parseOCIRef(), parseWWWAuthenticate(), fetchBearerToken(),
  fetchManifest() including 401 auth flow, extractTar() gzip/plain
  auto-detection, extractTarEntries() dir/symlink/traversal paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
…iden#319

- Add text language tag to fenced code block in SKILL.md
- Clarify HTTP(S) feature source error message in FromMap
- Update Order docstring: installsAfter soft-ignore is intentional per spec
- Reject symlinks and non-regular files in local feature copyDir
- Validate defaults with same type/enum rules as user options in Merge
- Replace symlink creation with error in extractTarEntries; add
  safeTarTarget helper for robust path-containment checks
- Update oci_test.go to expect error for symlink tar entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
…ners skill

Per the Dev Container spec, installsAfter is a soft ordering hint —
references to features not present in feats are silently ignored, not
errors. This was already the implemented behavior but the SKILL.md and
the test comment did not make the distinction explicit vs. missing
metadata entries (which are always errors).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
- localFeature.Download: resolve srcDir to an absolute path via
  filepath.Abs to prevent cwd-relative path confusion
- downloadAndExtractLayer: buffer blob to a temp file, verify its
  SHA-256 digest before extraction to detect corrupt/tampered blobs;
  reject unknown digest algorithms upfront
- oci.go Close() calls: propagate gzip reader close errors via named
  return; check file close errors after writes; explicitly discard
  response body close errors with _ = to satisfy errcheck
- safeTarTarget: allow "." (archive root entry) to map to destDir,
  fixing extraction of real OCI feature archives that include a ./
  dir entry; reject absolute paths and ".." traversals as before
- Update affected tests to supply real SHA-256 digests for test blobs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
Adds tests for the previously uncovered error paths in local.go:
- Download returns error when destDir exists as a regular file
- Download wraps copyDir errors with "copying feature from" context
- copyDir rejects symlinks and non-regular files (named pipes, etc.)
- copyFile returns error on unreadable source or read-only destination
- ../ relative paths resolve correctly against the workspace config dir
- Feature directories with subdirectories are copied recursively

Moves local.go coverage from ~70% to 90%+ across all three functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
…iden#319

Production fixes:
- copyFile uses named return to propagate Close errors (errcheck +
  delayed write errors from out.Close() now surface correctly)
- defer os.Remove(tmpPath) explicitly discards error to satisfy errcheck

Test fixes across features_test.go, oci_test.go, oci_integration_test.go,
local_test.go, and local_unix_test.go:
- t.Error → t.Fatal before err.Error() calls (prevent nil-panic)
- tw/gw Close calls checked with t.Fatalf (archive bytes must be valid)
- Handler fmt.Fprint/w.Write explicitly discarded with _, _ = (errcheck)
- Symlink test target replaced from /etc/hosts with a temp file (cross-platform)
- Integration tests use context.WithTimeout(2m) instead of context.Background()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Philippe Martin <phmartin@redhat.com>
@feloy feloy merged commit d0f405f into openkaiden:main Apr 23, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

devcontainers/features: Feature interface supporting OCI registry and local file tree

3 participants