diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b4d6e8..acbbd7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: "node", "playwright-deps", "sonar-scanner-cli", + "system-packages", "timezone", "vault-cli", "zig" diff --git a/README.md b/README.md index 036ef4f..b333188 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Below is a list with included features, click on the link for more details. | [node](./features/src/node/README.md) | A package which installs Node.js. | | [playwright-deps](./features/src/playwright-deps/README.md) | A package which installs the needed dependencies to run Playwright. | | [sonar-scanner-cli](./features/src/sonar-scanner-cli/README.md) | A package which installs the SonarScanner CLI. | +| [system-packages](./features/src/system-packages/README.md) | Install arbitrary system packages using apt or apk. | | [timezone](./features/src/timezone/README.md) | A package which allows setting the timezone. | | [vault-cli](./features/src/vault-cli/README.md) | A feature which installs the Vault CLI. | | [zig](./features/src/zig/README.md) | A feature which installs Zig. | diff --git a/build/build.go b/build/build.go index eec263c..5ab9649 100644 --- a/build/build.go +++ b/build/build.go @@ -278,6 +278,17 @@ func init() { return publishFeature("sonar-scanner-cli") }) + ////////// system-packages + gotaskr.Task("Feature:system-packages:Package", func() error { + return packageFeature("system-packages") + }) + gotaskr.Task("Feature:system-packages:Test", func() error { + return testFeature("system-packages") + }) + gotaskr.Task("Feature:system-packages:Publish", func() error { + return publishFeature("system-packages") + }) + ////////// timezone gotaskr.Task("Feature:timezone:Package", func() error { return packageFeature("timezone") diff --git a/features/src/system-packages/NOTES.md b/features/src/system-packages/NOTES.md new file mode 100644 index 0000000..f0525fa --- /dev/null +++ b/features/src/system-packages/NOTES.md @@ -0,0 +1,5 @@ +## Notes + +### System Compatibility + +Debian, Ubuntu, Alpine diff --git a/features/src/system-packages/README.md b/features/src/system-packages/README.md new file mode 100755 index 0000000..a229b50 --- /dev/null +++ b/features/src/system-packages/README.md @@ -0,0 +1,25 @@ +# System Packages (system-packages) + +Install arbitrary system packages using apt or apk. + +## Example Usage + +```json +"features": { + "ghcr.io/postfinance/devcontainer-features/system-packages:0.1.0": { + "packages": "" + } +} +``` + +## Options + +| Option | Description | Type | Default Value | Proposals | +|-----|-----|-----|-----|-----| +| packages | Comma-separated list of system packages to install. | string | <empty> | curl,git,htop, sshpass,yamllint,yq,gettext-base | + +## Notes + +### System Compatibility + +Debian, Ubuntu, Alpine diff --git a/features/src/system-packages/devcontainer-feature.json b/features/src/system-packages/devcontainer-feature.json new file mode 100644 index 0000000..50f6c79 --- /dev/null +++ b/features/src/system-packages/devcontainer-feature.json @@ -0,0 +1,17 @@ +{ + "id": "system-packages", + "version": "0.1.0", + "name": "System Packages", + "description": "Install arbitrary system packages using apt or apk.", + "options": { + "packages": { + "type": "string", + "description": "Comma-separated list of system packages to install.", + "proposals": [ + "curl,git,htop", + "sshpass,yamllint,yq,gettext-base" + ], + "default": "" + } + } +} \ No newline at end of file diff --git a/features/src/system-packages/install.sh b/features/src/system-packages/install.sh new file mode 100644 index 0000000..1a0580d --- /dev/null +++ b/features/src/system-packages/install.sh @@ -0,0 +1,4 @@ +. ./functions.sh + +"./installer_$(detect_arch)" \ + -packages="${PACKAGES:-""}" diff --git a/features/src/system-packages/installer.go b/features/src/system-packages/installer.go new file mode 100644 index 0000000..0c226ac --- /dev/null +++ b/features/src/system-packages/installer.go @@ -0,0 +1,60 @@ +package main + +import ( + "builder/installer" + "flag" + "fmt" + "os" + "strings" + + "github.com/roemer/gover" +) + +func main() { + if err := runMain(); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} + +func runMain() error { + packagesFlag := flag.String("packages", "", "Comma-separated list of system packages to install.") + flag.Parse() + + packages := parsePackages(*packagesFlag) + if len(packages) == 0 { + fmt.Println("No packages specified for installation.") + return nil + } + + feature := installer.NewFeature("System Packages", true, + &systemPackagesComponent{ + ComponentBase: installer.NewComponentBase("System Packages", "system-default"), + Packages: packages, + }) + return feature.Process() +} + +type systemPackagesComponent struct { + *installer.ComponentBase + Packages []string +} + +func (c *systemPackagesComponent) InstallVersion(version *gover.Version) error { + return installer.Tools.System.InstallPackages(c.Packages) +} + +func parsePackages(flagValue string) []string { + if flagValue == "" { + return nil + } + parts := strings.Split(flagValue, ",") + var pkgs []string + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + pkgs = append(pkgs, trimmed) + } + } + return pkgs +} diff --git a/features/test/system-packages/ci-utility.sh b/features/test/system-packages/ci-utility.sh new file mode 100644 index 0000000..bad0094 --- /dev/null +++ b/features/test/system-packages/ci-utility.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_command_exists "yq" +check_command_exists "yamllint" +check_command_exists "sshpass" diff --git a/features/test/system-packages/scenarios.json b/features/test/system-packages/scenarios.json new file mode 100644 index 0000000..0722a86 --- /dev/null +++ b/features/test/system-packages/scenarios.json @@ -0,0 +1,15 @@ +{ + "ci-utility": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./system-packages": { + "packages": "yq,yamllint,sshpass" + } + } + } +} \ No newline at end of file diff --git a/features/test/system-packages/test-images.json b/features/test/system-packages/test-images.json new file mode 100644 index 0000000..5d01b14 --- /dev/null +++ b/features/test/system-packages/test-images.json @@ -0,0 +1,6 @@ +[ + "mcr.microsoft.com/devcontainers/base:debian-12", + "mcr.microsoft.com/devcontainers/base:debian-13", + "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + "mcr.microsoft.com/devcontainers/base:alpine" +] \ No newline at end of file diff --git a/installer/apk.go b/installer/apk.go new file mode 100644 index 0000000..70ab503 --- /dev/null +++ b/installer/apk.go @@ -0,0 +1,27 @@ +package installer + +import ( + "strings" + + "github.com/roemer/gotaskr/execr" +) + +type apk struct{} + +func (a apk) InstallDependencies(dependencies ...string) error { + args := append([]string{"add", "--no-cache"}, dependencies...) + if err := execr.Run(true, "apk", args...); err != nil { + return err + } + return nil +} + +func (a apk) InstallLocalPackage(packagePath string) error { + if !strings.HasPrefix(packagePath, "./") { + packagePath = "./" + packagePath + } + if err := execr.Run(true, "apk", "add", "--no-cache", packagePath); err != nil { + return err + } + return nil +} diff --git a/installer/installer_tools.go b/installer/installer_tools.go index 800c175..2a98b29 100644 --- a/installer/installer_tools.go +++ b/installer/installer_tools.go @@ -11,6 +11,7 @@ type tools struct { System *system Versioning *versioning Apt *apt + Apk *apk } var Tools *tools @@ -27,5 +28,6 @@ func init() { System: &system{}, Versioning: &versioning{}, Apt: &apt{}, + Apk: &apk{}, } } diff --git a/installer/system.go b/installer/system.go index f0d52aa..37f8397 100644 --- a/installer/system.go +++ b/installer/system.go @@ -15,6 +15,21 @@ const ( type system struct{} +func (s *system) InstallPackages(packages []string) error { + osInfo, err := s.GetOsInfo() + if err != nil { + return err + } + switch { + case osInfo.IsDebian(), osInfo.IsUbuntu(): + return Tools.Apt.InstallDependencies(packages...) + case osInfo.IsAlpine(): + return Tools.Apk.InstallDependencies(packages...) + default: + return fmt.Errorf("unsupported OS vendor: %s", osInfo.Vendor) + } +} + func (s *system) MapArchitecture(mapping map[string]string) (string, error) { mappedValue, ok := mapping[runtime.GOARCH] if !ok { @@ -58,6 +73,10 @@ func (v *OsInfo) IsUbuntu() bool { return v.Vendor == "ubuntu" } +func (v *OsInfo) IsAlpine() bool { + return v.Vendor == "alpine" +} + func (v *OsInfo) MajorVersion() int { var major int fmt.Sscanf(v.VersionId, "%d", &major)