Conversation
Signed-off-by: Mah, Yock Gen <yock.gen.mah@intel.com>
Signed-off-by: Mah, Yock Gen <yock.gen.mah@intel.com>
Signed-off-by: Mah, Yock Gen <yock.gen.mah@intel.com>
Signed-off-by: Mah, Yock Gen <yock.gen.mah@intel.com>
Signed-off-by: Mah, Yock Gen <yock.gen.mah@intel.com>
There was a problem hiding this comment.
Pull request overview
Adds support for using local filesystem directories as package repositories (via a new path field) by generating/serving temporary DEB/RPM repositories over HTTP, and updates schema + tests to validate the new template shape.
Changes:
- Introduces a temporary HTTP file server helper to serve local repositories.
- Adds
pathsupport to the image template schema/config and wires local repo package discovery into DEB/RPM download flows. - Expands unit tests across providers, config/schema validation, and package utility helpers.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/utils/network/httpserver.go | New helper to serve a repository directory over HTTP with shutdown support. |
| internal/utils/network/httpserver_test.go | Tests for the temporary HTTP server behavior. |
| internal/provider/ubuntu/ubuntu.go | Adds dpkg-scanpackages host dependency needed for local DEB repo metadata generation. |
| internal/provider/ubuntu/ubuntu_test.go | Adds tests for repo list building and error wrapping/host dependency behaviors. |
| internal/provider/rcd/rcd_test.go | Adds tests for PostProcess cleanup and host dependency behaviors (and related mocks). |
| internal/provider/elxr/elxr.go | Adds dpkg-scanpackages host dependency for local DEB repo metadata generation. |
| internal/provider/elxr/elxr_test.go | Adds tests for PostProcess cleanup and host dependency behaviors (and related mocks). |
| internal/ospackage/rpmutils/verify.go | Adds special-casing for [trusted=yes] during RPM verification. |
| internal/ospackage/rpmutils/helper.go | Adds RPM CreateTemporaryRepository to build + serve a local RPM repo. |
| internal/ospackage/rpmutils/helper_test.go | Adds tests for local RPM repo creation helpers and local user package handling. |
| internal/ospackage/rpmutils/download.go | Adds Path field + local repo package ingestion (LocalUserPackages) into RPM download flow. |
| internal/ospackage/debutils/zip_test.go | Adds tests for XZ decompression and dispatcher behavior. |
| internal/ospackage/debutils/resolver_internal_test.go | Adds tests for internal resolver/helper functions (glob/filter/url join). |
| internal/ospackage/debutils/helper.go | Adds DEB CreateTemporaryRepository to build + serve a local Debian repo (Packages/Release). |
| internal/ospackage/debutils/helper_test.go | Adds tests for DEB temp repo creation and local user package handling. |
| internal/ospackage/debutils/download.go | Adds Path field and local repo package ingestion (LocalUserPackages) into DEB download flow. |
| internal/image/imageos/imageos_test.go | Adds additional error-path tests for initramfs/sysfs/ESP/verity helper logic. |
| internal/config/version/version_test.go | Adds tests asserting default build metadata values. |
| internal/config/validate/validate_test.go | Adds schema validation test for path-based repositories with [trusted=yes]. |
| internal/config/schema/os-image-template.schema.json | Extends schema to allow path repositories and enforces url XOR path. |
| internal/config/schema/embed_test.go | Adds tests ensuring embedded schemas are present and valid JSON with expected metadata. |
| internal/config/config.go | Adds path to PackageRepository and a ValidatePackageRepository helper. |
| internal/config/config_test.go | Adds YAML parsing test for path-based repositories. |
| internal/config/apt_sources_test.go | Adds tests ensuring path-only repos don’t generate apt sources; adds nil-input test for normalization. |
| image-templates/ubuntu24-x86_64-edge-raw.yml | Adds commented example for a local path-based repo. |
| // Create temporary repository directory | ||
| tempRepoPath := filepath.Join("/tmp", fmt.Sprintf("rpmrepo_%s_%d", repoName, time.Now().Unix())) | ||
| if err := os.MkdirAll(tempRepoPath, 0755); err != nil { | ||
| return "", "", nil, fmt.Errorf("failed to create temporary repository directory: %w", err) | ||
| } |
There was a problem hiding this comment.
The temporary repo directory name is predictable (/tmp/..._<unix seconds>) and is created with MkdirAll. In a multi-user environment this is vulnerable to pre-creation/symlink attacks and can collide under concurrency. Prefer os.MkdirTemp (or similar) to create a unique, securely-created directory under the OS temp dir.
| tempRepoPath := filepath.Join("/tmp", fmt.Sprintf("debrepo_%s_%d", repoName, time.Now().Unix())) | ||
| if err := os.MkdirAll(tempRepoPath, 0755); err != nil { |
There was a problem hiding this comment.
The temporary repo directory name is predictable (/tmp/..._<unix seconds>) and is created with MkdirAll. In a multi-user environment this is vulnerable to pre-creation/symlink attacks and can collide under concurrency. Prefer os.MkdirTemp (or similar) to create a unique, securely-created directory under the OS temp dir.
| tempRepoPath := filepath.Join("/tmp", fmt.Sprintf("debrepo_%s_%d", repoName, time.Now().Unix())) | |
| if err := os.MkdirAll(tempRepoPath, 0755); err != nil { | |
| safeRepoName := strings.NewReplacer(" ", "_", string(os.PathSeparator), "_").Replace(repoName) | |
| tempRepoPath, err := os.MkdirTemp("", "debrepo_"+safeRepoName+"_") | |
| if err != nil { |
| // Validate repository path exists | ||
| if _, err := os.Stat(repoPath); os.IsNotExist(err) { | ||
| return "", nil, fmt.Errorf("repository directory does not exist: %s", repoPath) | ||
| } else if err != nil { | ||
| return "", nil, fmt.Errorf("failed to access repository directory: %w", err) | ||
| } | ||
|
|
There was a problem hiding this comment.
repoPath is only checked for existence, but not that it is a directory. If a file path is provided, http.Dir(repoPath) won’t behave as intended and the error message (“repository directory…”) becomes misleading. Consider verifying Stat().IsDir() and returning a clear error when the path is not a directory.
| // Validate repository path exists | |
| if _, err := os.Stat(repoPath); os.IsNotExist(err) { | |
| return "", nil, fmt.Errorf("repository directory does not exist: %s", repoPath) | |
| } else if err != nil { | |
| return "", nil, fmt.Errorf("failed to access repository directory: %w", err) | |
| } | |
| // Validate repository path exists and is a directory | |
| repoInfo, err := os.Stat(repoPath) | |
| if os.IsNotExist(err) { | |
| return "", nil, fmt.Errorf("repository directory does not exist: %s", repoPath) | |
| } else if err != nil { | |
| return "", nil, fmt.Errorf("failed to access repository directory: %w", err) | |
| } | |
| if !repoInfo.IsDir() { | |
| return "", nil, fmt.Errorf("repository path is not a directory: %s", repoPath) | |
| } |
| listener, err := net.Listen("tcp", ":0") | ||
| if err != nil { | ||
| return "", nil, fmt.Errorf("failed to find available port: %w", err) | ||
| } | ||
| port := listener.Addr().(*net.TCPAddr).Port |
There was a problem hiding this comment.
Selecting a port by listening on :0 and then closing the listener introduces a race: another process can claim the port before ListenAndServe re-binds. Prefer creating the listener on 127.0.0.1:0 and starting the server via server.Serve(listener) so the same bound socket is used.
| server := &http.Server{ | ||
| Addr: ":" + strconv.Itoa(port), | ||
| Handler: mux, |
There was a problem hiding this comment.
Addr: ":<port>" binds on all interfaces, which can expose the served repository to the network even though the returned URL is localhost. Bind explicitly to 127.0.0.1 (or localhost) to keep this temporary server local-only.
|
|
||
| // In LocalUserPackages(), before the main processing loop: | ||
| // for _, rpItx := range localRepo { | ||
| // if rpItx.Path == "" { | ||
| // continue | ||
| // } | ||
| // | ||
| // // Check if it's a proper repository | ||
| // repoMetaDataPath := filepath.Join(rpItx.Path, "repodata/repomd.xml") | ||
| // if _, err := os.Stat(repoMetaDataPath); os.IsNotExist(err) { | ||
| // // Not a proper repo - need to create one | ||
| // tempRepoPath, _, cleanup, err := CreateTemporaryRepository(rpItx.Path, rpItx.Name) | ||
| // if err != nil { | ||
| // return nil, fmt.Errorf("failed to create temporary repository: %w", err) | ||
| // } | ||
| // // Store cleanup function for later use if needed | ||
| // _ = cleanup | ||
| // // Update the path to point to the new temp repo | ||
| // rpItx.Path = tempRepoPath | ||
| // } | ||
| // | ||
| // // Continue with existing repo processing... | ||
| // } |
There was a problem hiding this comment.
This commented-out pseudo-code block appears to be leftover implementation notes and makes the file harder to maintain. Please remove it now that LocalUserPackages() is implemented.
| // In LocalUserPackages(), before the main processing loop: | |
| // for _, rpItx := range localRepo { | |
| // if rpItx.Path == "" { | |
| // continue | |
| // } | |
| // | |
| // // Check if it's a proper repository | |
| // repoMetaDataPath := filepath.Join(rpItx.Path, "repodata/repomd.xml") | |
| // if _, err := os.Stat(repoMetaDataPath); os.IsNotExist(err) { | |
| // // Not a proper repo - need to create one | |
| // tempRepoPath, _, cleanup, err := CreateTemporaryRepository(rpItx.Path, rpItx.Name) | |
| // if err != nil { | |
| // return nil, fmt.Errorf("failed to create temporary repository: %w", err) | |
| // } | |
| // // Store cleanup function for later use if needed | |
| // _ = cleanup | |
| // // Update the path to point to the new temp repo | |
| // rpItx.Path = tempRepoPath | |
| // } | |
| // | |
| // // Continue with existing repo processing... | |
| // } |
| log := logger.Logger() | ||
| //ignore verification if trusted=yes | ||
| if pubkeyPath == "[trusted=yes]" { | ||
| log.Infof("Repository marked (%s) as [trusted=yes], skipping Release file signature verification", rpmPath) | ||
| return nil |
There was a problem hiding this comment.
[trusted=yes] handling here is currently ineffective: createTempGPGKeyFiles() skips entries equal to [trusted=yes], so VerifyAll() will never pass that value into verifyWithGoRpm(). If the intent is to allow skipping RPM signature verification, handle it explicitly in Validate() (e.g., detect the sentinel and bypass verification) and update the log message to refer to RPM signature verification (not “Release file”).
|
|
||
| // ValidatePackageRepository validates that either URL or Path is provided | ||
| func (pr *PackageRepository) ValidatePackageRepository() error { | ||
| if pr.URL == "" && pr.Path == "" { | ||
| return fmt.Errorf("repository '%s': either 'url' or 'path' must be provided", pr.Codename) | ||
| } | ||
| if pr.URL != "" && pr.Path != "" { | ||
| return fmt.Errorf("repository '%s': cannot specify both 'url' and 'path', choose one", pr.Codename) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
ValidatePackageRepository is added but isn’t called anywhere in the codebase, so the URL-vs-Path constraint is not enforced at runtime unless schema validation is always run beforehand. Either wire this into template load/validation (e.g., validate repositories during LoadTemplate), or remove the unused method to avoid dead code.
| // ValidatePackageRepository validates that either URL or Path is provided | |
| func (pr *PackageRepository) ValidatePackageRepository() error { | |
| if pr.URL == "" && pr.Path == "" { | |
| return fmt.Errorf("repository '%s': either 'url' or 'path' must be provided", pr.Codename) | |
| } | |
| if pr.URL != "" && pr.Path != "" { | |
| return fmt.Errorf("repository '%s': cannot specify both 'url' and 'path', choose one", pr.Codename) | |
| } | |
| return nil | |
| } |
| // TestCreateTemporaryRepositorySuccess tests CreateTemporaryRepository with valid DEB files | ||
| func TestCreateTemporaryRepositorySuccess(t *testing.T) { |
There was a problem hiding this comment.
Despite the name, this test does not exercise or validate the success path. Rename it to reflect what it actually verifies, or create the expected metadata files on disk so the true success path is tested.
| // TestCreateTemporaryRepositorySuccess tests CreateTemporaryRepository with valid DEB files | |
| func TestCreateTemporaryRepositorySuccess(t *testing.T) { | |
| // TestCreateTemporaryRepositoryFailsWhenMetadataMissing tests that | |
| // CreateTemporaryRepository returns an error when mocked commands do not | |
| // create the repository metadata files the function validates. | |
| func TestCreateTemporaryRepositoryFailsWhenMetadataMissing(t *testing.T) { |
| // Test that the repository paths would be different (from the temp directory structure) | ||
| // Even though the function fails, the initial path creation should use unique names | ||
| t.Log("This test verifies unique temporary directory naming with mocked commands") | ||
| } |
There was a problem hiding this comment.
This test doesn’t assert anything about directory uniqueness (it only logs). Consider capturing and comparing the returned repoPath values to ensure they differ, or remove the test if it can’t make a meaningful assertion with the current mocking approach.
Merge Checklist
All boxes should be checked before merging the PR
Description
Any Newly Introduced Dependencies
How Has This Been Tested?