diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2ee11ba7..be58bdb8 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,12 +4,16 @@ ## General +* Added the ability to automatically copy files into the built images filesystem + ## API ### Image Definition Changes ### Image Configuration Directory Changes +* An optional directory named `os-files` may be included to copy files into the resulting image's filesystem at runtime + ## Bug Fixes --- diff --git a/docs/building-images.md b/docs/building-images.md index 928615bf..cc0ce55c 100644 --- a/docs/building-images.md +++ b/docs/building-images.md @@ -473,6 +473,24 @@ the built image and used to register with Elemental on boot. > ```podman run``` command. For more info on why this is required, please see > [Package resolution design](design/pkg-resolution.md#running-the-eib-container). +## Operating System Files + +Files placed in the `os-files` directory in the image configuration directory will be automatically copied +into the filesystem of the built image. The exact directory structure will be retained when they are copied. +For example, if a file exists in a subdirectory named `os-files/etc`, it will be placed in the `/etc` directory +of the built image. + +If the `os-files` directory exists, it cannot be empty. + +```bash +. +├── definition.yaml +└── os-files + └── etc + └── ssh + └── sshd_config +``` + ## Custom EIB has the ability to bundle in custom scripts that will be run during the combustion phase when a node is diff --git a/pkg/combustion/combustion.go b/pkg/combustion/combustion.go index f6f8929f..035d17fc 100644 --- a/pkg/combustion/combustion.go +++ b/pkg/combustion/combustion.go @@ -120,6 +120,10 @@ func (c *Combustion) Configure(ctx *image.Context) error { name: rpmComponentName, runnable: c.configureRPMs, }, + { + name: osFilesComponentName, + runnable: configureOSFiles, + }, { name: systemdComponentName, runnable: configureSystemd, diff --git a/pkg/combustion/osfiles.go b/pkg/combustion/osfiles.go new file mode 100644 index 00000000..bb0937d1 --- /dev/null +++ b/pkg/combustion/osfiles.go @@ -0,0 +1,77 @@ +package combustion + +import ( + _ "embed" + "fmt" + "os" + "path/filepath" + + "github.com/suse-edge/edge-image-builder/pkg/fileio" + "github.com/suse-edge/edge-image-builder/pkg/image" + "github.com/suse-edge/edge-image-builder/pkg/log" + "go.uber.org/zap" +) + +const ( + osFilesComponentName = "os files" + osFilesConfigDir = "os-files" + osFilesScriptName = "19-copy-os-files.sh" + osFilesLogFile = "copy-os-files.log" +) + +var ( + //go:embed templates/19-copy-os-files.sh + osFilesScript string +) + +func configureOSFiles(ctx *image.Context) ([]string, error) { + if !isComponentConfigured(ctx, osFilesConfigDir) { + log.AuditComponentSkipped(osFilesComponentName) + zap.S().Info("skipping os files component, no files provided") + return nil, nil + } + + if err := copyOSFiles(ctx); err != nil { + log.AuditComponentFailed(osFilesComponentName) + return nil, err + } + + if err := writeOSFilesScript(ctx); err != nil { + log.AuditComponentFailed(osFilesComponentName) + return nil, err + } + + log.AuditComponentSuccessful(osFilesComponentName) + return []string{osFilesScriptName}, nil +} + +func copyOSFiles(ctx *image.Context) error { + srcDirectory := filepath.Join(ctx.ImageConfigDir, osFilesConfigDir) + destDirectory := filepath.Join(ctx.CombustionDir, osFilesConfigDir) + + dirEntries, err := os.ReadDir(srcDirectory) + if err != nil { + return fmt.Errorf("reading the os files directory at %s: %w", srcDirectory, err) + } + + // If the directory exists but there's nothing in it, consider it an error case + if len(dirEntries) == 0 { + return fmt.Errorf("no files found in directory %s", srcDirectory) + } + + if err := fileio.CopyFiles(srcDirectory, destDirectory, "", true); err != nil { + return fmt.Errorf("copying os-files: %w", err) + } + + return nil +} + +func writeOSFilesScript(ctx *image.Context) error { + osFilesScriptFilename := filepath.Join(ctx.CombustionDir, osFilesScriptName) + + if err := os.WriteFile(osFilesScriptFilename, []byte(osFilesScript), fileio.ExecutablePerms); err != nil { + return fmt.Errorf("writing os files script %s: %w", osFilesScriptFilename, err) + } + + return nil +} diff --git a/pkg/combustion/osfiles_test.go b/pkg/combustion/osfiles_test.go new file mode 100644 index 00000000..f4eee663 --- /dev/null +++ b/pkg/combustion/osfiles_test.go @@ -0,0 +1,71 @@ +package combustion + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/suse-edge/edge-image-builder/pkg/image" +) + +func setupOsFilesConfigDir(t *testing.T, empty bool) (ctx *image.Context, teardown func()) { + ctx, teardown = setupContext(t) + + testOsFilesDir := filepath.Join(ctx.ImageConfigDir, osFilesConfigDir) + err := os.Mkdir(testOsFilesDir, 0o755) + require.NoError(t, err) + + if !empty { + nestedOsFilesDir := filepath.Join(testOsFilesDir, "etc", "ssh") + err = os.MkdirAll(nestedOsFilesDir, 0o755) + require.NoError(t, err) + + testFile := filepath.Join(nestedOsFilesDir, "test-config-file") + _, err = os.Create(testFile) + require.NoError(t, err) + } + + return +} + +func TestConfigureOSFiles(t *testing.T) { + // Setup + ctx, teardown := setupOsFilesConfigDir(t, false) + defer teardown() + + // Test + scriptNames, err := configureOSFiles(ctx) + + // Verify + require.NoError(t, err) + + assert.Equal(t, []string{osFilesScriptName}, scriptNames) + + // -- Combustion Script + expectedCombustionScript := filepath.Join(ctx.CombustionDir, osFilesScriptName) + contents, err := os.ReadFile(expectedCombustionScript) + require.NoError(t, err) + assert.Contains(t, string(contents), "cp -R") + + // -- Files + expectedFile := filepath.Join(ctx.CombustionDir, osFilesConfigDir, "etc", "ssh", "test-config-file") + assert.FileExists(t, expectedFile) +} + +func TestConfigureOSFiles_EmptyDirectory(t *testing.T) { + // Setup + ctx, teardown := setupOsFilesConfigDir(t, true) + defer teardown() + + // Test + scriptName, err := configureOSFiles(ctx) + + // Verify + assert.Nil(t, scriptName) + + srcDirectory := filepath.Join(ctx.ImageConfigDir, osFilesConfigDir) + assert.EqualError(t, err, fmt.Sprintf("no files found in directory %s", srcDirectory)) +} diff --git a/pkg/combustion/templates/19-copy-os-files.sh b/pkg/combustion/templates/19-copy-os-files.sh new file mode 100644 index 00000000..19bb76bc --- /dev/null +++ b/pkg/combustion/templates/19-copy-os-files.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail + +cp -R ./os-files/* /