Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package build
import (
"context"
"fmt"
"path/filepath"
"strings"

"github.com/suse/elemental/v3/internal/config"
Expand Down Expand Up @@ -141,7 +142,12 @@ func newDeployment(
return nil, fmt.Errorf("computing configuration partition size: %w", err)
}

deploymentOpts = append(deploymentOpts, deployment.WithConfigPartition(deployment.MiB(configSize)))
configLabel := deployment.CatalystLabel
if ignDir, _ := vfs.Exists(system.FS(), filepath.Join(output.FirstbootConfigDir(), image.IgnitionFilePath())); ignDir {
configLabel = deployment.IgnitionLabel
}

deploymentOpts = append(deploymentOpts, deployment.WithConfigPartition(deployment.MiB(configSize), configLabel))
}

d := deployment.New(deploymentOpts...)
Expand Down
5 changes: 3 additions & 2 deletions internal/cli/action/customize.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,12 @@ func setupCustomizeRunner(

return &customize.Runner{
System: s,
ConfigManager: setupConfigManager(s, args.ConfigDir, output, args.Local),
ConfigManager: setupConfigManager(s, args.ConfigDir, output, args.BaseConfig, args.Local),
FileExtractor: extr,
}, nil
}

func setupConfigManager(s *sys.System, configDir string, output config.Output, local bool) *config.Manager {
func setupConfigManager(s *sys.System, configDir string, output config.Output, baseConfig, local bool) *config.Manager {
valuesResolver := &helm.ValuesResolver{
FS: s.FS(),
ValuesDir: v0.Dir(configDir).HelmValuesDir(),
Expand All @@ -139,6 +139,7 @@ func setupConfigManager(s *sys.System, configDir string, output config.Output, l
config.NewHelm(s.FS(), valuesResolver, s.Logger(), output.OverlaysDir()),
config.WithDownloadFunc(http.DownloadFile),
config.WithLocal(local),
config.WithBaseConfig(baseConfig),
)
}

Expand Down
6 changes: 6 additions & 0 deletions internal/cli/cmd/customize.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type CustomizeFlags struct {
Platform string
MediaType string
Local bool
BaseConfig bool
}

var CustomizeArgs CustomizeFlags
Expand Down Expand Up @@ -90,6 +91,11 @@ func NewCustomizeCommand(appName string, action func(context.Context, *cli.Comma
Usage: localDesc,
Destination: &CustomizeArgs.Local,
},
&cli.BoolFlag{
Name: "base-config",
Usage: "Sets the Ignition configuration to be included as the base configuration",
Destination: &CustomizeArgs.BaseConfig,
},
},
}
}
4 changes: 4 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func (o Output) ISOStoreDir() string {
return filepath.Join(o.ExtractedFilesStoreDir(), "ISOs")
}

func (o Output) InitrdExtensionFile() string {
return filepath.Join(o.RootPath, "initrdExt.cpio")
}

func (o Output) Cleanup(fs vfs.FS) error {
return fs.RemoveAll(o.RootPath)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ limitations under the License.
package config_test

import (
"path/filepath"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

Expand All @@ -26,6 +28,13 @@ import (
sysmock "github.com/suse/elemental/v3/pkg/sys/mock"
)

var _ = Describe("Output", func() {
It("InitrdExtensionFile returns the CPIO path relative to RootPath", func() {
output := config.Output{RootPath: "/my/root"}
Expect(output.InitrdExtensionFile()).To(Equal(filepath.Join("/my/root", "initrdExt.cpio")))
})
})

var _ = Describe("Schema", func() {

It("Successfully loads a schema version", func() {
Expand Down
75 changes: 63 additions & 12 deletions internal/config/ignition.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ limitations under the License.
package config

import (
"context"
_ "embed"
"fmt"
"path/filepath"
Expand All @@ -27,12 +28,14 @@ import (
"go.yaml.in/yaml/v3"

"github.com/suse/elemental/v3/internal/butane"
"github.com/suse/elemental/v3/internal/cpio"
"github.com/suse/elemental/v3/internal/image"
"github.com/suse/elemental/v3/internal/image/kubernetes"
"github.com/suse/elemental/v3/internal/template"
"github.com/suse/elemental/v3/pkg/extensions"
"github.com/suse/elemental/v3/pkg/manifest/api"
"github.com/suse/elemental/v3/pkg/sys"
"github.com/suse/elemental/v3/pkg/sys/vfs"
)

const (
Expand All @@ -41,6 +44,8 @@ const (
updateLinkerCacheUnitName = "update-linker-cache.service"
k8sResourcesUnitName = "k8s-resource-installer.service"
k8sConfigUnitName = "k8s-config-installer.service"
ignitionFileName = "10-elemental.ign"
ignitionFromButaneFileName = "90-butane.ign"
)

var (
Expand Down Expand Up @@ -68,6 +73,9 @@ var (
// * Kubernetes configuration and deployment files
// * Systemd extensions
// * Kubernetes distribution installation
//
// if baseConfig is set to true it builds a CPIO file containing the Ignition configuration at /usr/lib/ignition/base.d
// the CPIO file can be used as an initrd extension allowing the user to provide user configuration that is merged on top.
func (m *Manager) configureIgnition(conf *image.Configuration, output Output, k8sScript, k8sConfScript string, ext []api.SystemdExtension) error {
if len(conf.ButaneConfig) == 0 &&
k8sScript == "" &&
Expand All @@ -86,18 +94,6 @@ func (m *Manager) configureIgnition(conf *image.Configuration, output Output, k8
config.Variant = variant
config.Version = version

if len(conf.ButaneConfig) > 0 {
m.system.Logger().Info("Translating butane configuration to Ignition syntax")

ignitionBytes, err := butane.TranslateBytes(m.system, conf.ButaneConfig)
if err != nil {
return fmt.Errorf("failed translating butane configuration: %w", err)
}
config.MergeInlineIgnition(string(ignitionBytes))
} else {
m.system.Logger().Info("No butane configuration to translate into Ignition syntax")
}

if k8sScript != "" {
initHostname := "*"
if len(conf.Kubernetes.Nodes) > 0 {
Expand Down Expand Up @@ -142,10 +138,65 @@ func (m *Manager) configureIgnition(conf *image.Configuration, output Output, k8
config.AddSystemdUnit(updateLinkerCacheUnitName, updateLinkerCacheUnit, true)
}

if m.baseConfig {
return m.writeBaseIgnitionConfig(output, config, conf.ButaneConfig)
}

return m.writeUserIgnitionConfig(output, config, conf.ButaneConfig)
}

// writeUserIgnitionConfig renders the Ignition configuration including the provided butane configuration as a single
// <ignition_device>/ignition/config.ign file. From Ignition's PoV this represents the user configuration which gets merged
// with the stock configuration, if any.
func (m *Manager) writeUserIgnitionConfig(output Output, config butane.Config, butaneConfing map[string]any) error {
if len(butaneConfing) > 0 {
m.system.Logger().Info("Translating butane configuration to Ignition syntax")

ignitionBytes, err := butane.TranslateBytes(m.system, butaneConfing)
if err != nil {
return fmt.Errorf("failed translating butane configuration: %w", err)
}
config.MergeInlineIgnition(string(ignitionBytes))
}

ignitionFile := filepath.Join(output.FirstbootConfigDir(), image.IgnitionFilePath())
return butane.WriteIgnitionFile(m.system, config, ignitionFile)
}

// writeBaseIgnitionConfig renders the generated Ignition configuration including the user provided butane configuration
// as part of a CPIO file, which can be used to extend the OS initrd and include ignition base configuration the expected
// /usr/lib/ignition/base.d path
func (m *Manager) writeBaseIgnitionConfig(output Output, config butane.Config, butaneConfing map[string]any) (err error) {
tmpDir, err := vfs.TempDir(m.system.FS(), output.RootPath, "initrd")
if err != nil {
return fmt.Errorf("creating temporary directory for ignition initrd extension: %w", err)
}
defer func() {
e := m.system.FS().RemoveAll(tmpDir)
if err == nil && e != nil {
err = e
}
}()

ignitionFile := filepath.Join(tmpDir, image.IgnitionBaseConfigPath(), ignitionFileName)
err = butane.WriteIgnitionFile(m.system, config, ignitionFile)
if err != nil {
return fmt.Errorf("writing ignition file %q: %w", ignitionFile, err)
}

if len(butaneConfing) > 0 {
m.system.Logger().Info("Translating butane configuration to Ignition syntax")

butaneFile := filepath.Join(tmpDir, image.IgnitionBaseConfigPath(), ignitionFromButaneFileName)
err = butane.WriteIgnitionFile(m.system, butaneConfing, butaneFile)
if err != nil {
return fmt.Errorf("writing ignition file %q: %w", butaneFile, err)
}
}

return cpio.CreateCPIO(context.Background(), m.system, tmpDir, output.InitrdExtensionFile())
}

func generateK8sResourcesUnit(deployScript, initHostname string) (string, error) {
values := struct {
KubernetesDir string
Expand Down
55 changes: 55 additions & 0 deletions internal/config/ignition_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,59 @@ passwd:
Expect(ignition).To(ContainSubstring("merge"))
Expect(buffer.String()).To(ContainSubstring("translating Butane to Ignition reported non-fatal entries"))
})

Describe("Ignition configuration as base config", func() {
BeforeEach(func() {
m = NewManager(system, nil, WithBaseConfig(true))
})

It("Creates a CPIO initrd extension instead of a firstboot ignition file", func() {
conf := &image.Configuration{
Kubernetes: kubernetes.Kubernetes{
Config: kubernetes.Config{
RegistriesFilePath: "/etc/kubernetes/config/registries.yaml",
},
},
}

Expect(m.configureIgnition(conf, output, "", "k8sConfScript", nil)).To(Succeed())

ok, _ := vfs.Exists(system.FS(), filepath.Join(output.FirstbootConfigDir(), image.IgnitionFilePath()))
Expect(ok).To(BeFalse())

ok, _ = vfs.Exists(system.FS(), output.InitrdExtensionFile())
Expect(ok).To(BeTrue())

cpioContent, err := system.FS().ReadFile(output.InitrdExtensionFile())
Expect(err).NotTo(HaveOccurred())
Expect(string(cpioContent)).To(ContainSubstring(ignitionFileName))
Expect(string(cpioContent)).NotTo(ContainSubstring(ignitionFromButaneFileName))
})

It("Creates a CPIO with both elemental and butane ignition files when ButaneConfig is provided", func() {
var butaneConf map[string]any

butaneConfigString := `
version: 1.6.0
variant: fcos
passwd:
users:
- name: pipo
password_hash: $y$j9T$aUmgEDoFIDPhGxEe2FUjc/$C5A...
`
Expect(v0.ParseAny([]byte(butaneConfigString), &butaneConf)).To(Succeed())
conf := &image.Configuration{ButaneConfig: butaneConf}

Expect(m.configureIgnition(conf, output, "", "", nil)).To(Succeed())

ok, err := vfs.Exists(system.FS(), filepath.Join(output.FirstbootConfigDir(), image.IgnitionFilePath()))
Expect(err).NotTo(HaveOccurred())
Expect(ok).To(BeFalse())

cpioContent, err := system.FS().ReadFile(output.InitrdExtensionFile())
Expect(err).NotTo(HaveOccurred())
Expect(string(cpioContent)).To(ContainSubstring(ignitionFileName))
Expect(string(cpioContent)).To(ContainSubstring(ignitionFromButaneFileName))
})
})
})
10 changes: 10 additions & 0 deletions internal/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ type Manager struct {
system *sys.System
local bool

// baseConfig sets Ignition configuration to be included as base configuration
// allowing a user configuration to be merged on top
baseConfig bool

rmResolver releaseManifestResolver
downloadFile downloadFunc
unpackImage unpackFunc
Expand Down Expand Up @@ -79,6 +83,12 @@ func WithLocal(local bool) Opts {
}
}

func WithBaseConfig(baseConfig bool) Opts {
return func(m *Manager) {
m.baseConfig = baseConfig
}
}

func NewManager(sys *sys.System, helm helmConfigurator, opts ...Opts) *Manager {
m := &Manager{
system: sys,
Expand Down
Loading