diff --git a/.gitignore b/.gitignore index e8fabe8..84a0280 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode/ os-metal vendor -hack/tools/bin/* \ No newline at end of file +hack/tools/bin/* +ginkgo.report diff --git a/go.mod b/go.mod index 6864dca..bbc0c40 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/gardener/gardener v1.89.4 github.com/go-logr/logr v1.4.2 github.com/golang/mock v1.6.0 + github.com/google/go-cmp v0.6.0 github.com/metal-stack/gardener-extension-provider-metal v0.24.4 github.com/onsi/ginkgo/v2 v2.20.0 github.com/onsi/gomega v1.34.1 @@ -58,7 +59,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/pkg/controller/operatingsystemconfig/actuator.go b/pkg/controller/operatingsystemconfig/actuator.go index 511afbd..f7990bc 100644 --- a/pkg/controller/operatingsystemconfig/actuator.go +++ b/pkg/controller/operatingsystemconfig/actuator.go @@ -18,35 +18,76 @@ import ( "context" _ "embed" "fmt" + "slices" + "strings" "github.com/gardener/gardener/extensions/pkg/controller/operatingsystemconfig" oscommonactuator "github.com/gardener/gardener/extensions/pkg/controller/operatingsystemconfig/oscommon/actuator" + gardenv1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" "github.com/go-logr/logr" + metalextensionv1alpha1 "github.com/metal-stack/gardener-extension-provider-metal/pkg/apis/metal/v1alpha1" + "github.com/metal-stack/os-metal-extension/pkg/controller/operatingsystemconfig/generator" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" +) - "github.com/metal-stack/os-metal-extension/pkg/controller/operatingsystemconfig/generator" +const ( + containerdConfig = `# Generated by os-extension-metal +version = 2 +imports = ["/etc/containerd/conf.d/*.toml"] +disabled_plugins = [] + +[plugins."io.containerd.grpc.v1.cri".registry] + config_path = "/etc/containerd/certs.d" +` ) type actuator struct { - client client.Client + client client.Client + decoder runtime.Decoder } // NewActuator creates a new Actuator that updates the status of the handled OperatingSystemConfig resources. func NewActuator(mgr manager.Manager) operatingsystemconfig.Actuator { + scheme := runtime.NewScheme() + utilruntime.Must(gardenv1beta1.AddToScheme(scheme)) + decoder := serializer.NewCodecFactory(scheme).UniversalDecoder() + return &actuator{ - client: mgr.GetClient(), + client: mgr.GetClient(), + decoder: decoder, } } func (a *actuator) Reconcile(ctx context.Context, log logr.Logger, osc *extensionsv1alpha1.OperatingSystemConfig) ([]byte, *string, []string, []string, []extensionsv1alpha1.Unit, []extensionsv1alpha1.File, error) { + imageProviderConfig := &metalextensionv1alpha1.ImageProviderConfig{} + + networkIsolation := &metalextensionv1alpha1.NetworkIsolation{} + if osc.Spec.ProviderConfig != nil { + err := decodeProviderConfig(a.decoder, osc.Spec.ProviderConfig, imageProviderConfig) + if err != nil { + return nil, nil, nil, nil, nil, nil, fmt.Errorf("unable to decode providerConfig") + } + } + if imageProviderConfig.NetworkIsolation != nil { + networkIsolation = imageProviderConfig.NetworkIsolation + } + + extensionFiles := getExtensionFiles(osc, networkIsolation) + + osc.Spec.Files = EnsureFiles(osc.Spec.Files, extensionFiles...) + cloudConfig, command, err := oscommonactuator.CloudConfigFromOperatingSystemConfig(ctx, log, a.client, osc, generator.IgnitionGenerator()) if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("could not generate cloud config: %w", err) } - return cloudConfig, command, oscommonactuator.OperatingSystemConfigUnitNames(osc), oscommonactuator.OperatingSystemConfigFilePaths(osc), nil, nil, nil + return cloudConfig, command, oscommonactuator.OperatingSystemConfigUnitNames(osc), oscommonactuator.OperatingSystemConfigFilePaths(osc), nil, extensionFiles, nil } func (a *actuator) Delete(_ context.Context, _ logr.Logger, _ *extensionsv1alpha1.OperatingSystemConfig) error { @@ -64,3 +105,161 @@ func (a *actuator) ForceDelete(ctx context.Context, log logr.Logger, osc *extens func (a *actuator) Restore(ctx context.Context, log logr.Logger, osc *extensionsv1alpha1.OperatingSystemConfig) ([]byte, *string, []string, []string, []extensionsv1alpha1.Unit, []extensionsv1alpha1.File, error) { return a.Reconcile(ctx, log, osc) } + +func getExtensionFiles(osc *extensionsv1alpha1.OperatingSystemConfig, networkIsolation *metalextensionv1alpha1.NetworkIsolation) []extensionsv1alpha1.File { + var extensionFiles []extensionsv1alpha1.File + + if len(networkIsolation.RegistryMirrors) > 0 { + dnsFiles := additionalDNSConfFiles(networkIsolation.DNSServers) + extensionFiles = append(extensionFiles, dnsFiles...) + + ntpFiles := additionalNTPConfFiles(networkIsolation.NTPServers) + extensionFiles = append(extensionFiles, ntpFiles...) + } + + if osc.Spec.CRIConfig != nil && osc.Spec.CRIConfig.Name == extensionsv1alpha1.CRINameContainerD { + // the debian:12 containerd ships with "cri" plugin disabled, so we need override the config that ships with the os + // + // with g/g v1.100 it would be best to just remove the config.toml and let the GNA generate the default config. + // unfortunately, ignition does not allow to remove files easily. + // along with the default import paths. see https://github.com/gardener/gardener/pull/10050) + extensionFiles = append(extensionFiles, extensionsv1alpha1.File{ + Path: "/etc/containerd/config.toml", + Permissions: ptr.To(int32(0644)), + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: containerdConfig, + }, + }, + }) + + if len(networkIsolation.RegistryMirrors) > 0 { + extensionFiles = append(extensionFiles, additionalContainerdMirrors(networkIsolation.RegistryMirrors)...) + } + } + + return extensionFiles +} + +// decodeProviderConfig decodes the provider config into the given struct +func decodeProviderConfig(decoder runtime.Decoder, providerConfig *runtime.RawExtension, into runtime.Object) error { + if providerConfig == nil { + return nil + } + + if _, _, err := decoder.Decode(providerConfig.Raw, nil, into); err != nil { + return fmt.Errorf("could not decode provider config: %w", err) + } + + return nil +} + +func additionalContainerdMirrors(mirrors []metalextensionv1alpha1.RegistryMirror) []extensionsv1alpha1.File { + var files []extensionsv1alpha1.File + + for _, m := range mirrors { + for _, of := range m.MirrorOf { + content := fmt.Sprintf(`server = "https://%s" + +[host.%q] + capabilities = ["pull", "resolve"] +`, of, m.Endpoint) + + files = append(files, extensionsv1alpha1.File{ + Path: fmt.Sprintf("/etc/containerd/certs.d/%s/hosts.toml", of), + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: content, + }, + }, + }) + } + } + + return files +} + +func additionalDNSConfFiles(dnsServers []string) []extensionsv1alpha1.File { + if len(dnsServers) == 0 { + return nil + } + resolveDNS := strings.Join(dnsServers, " ") + systemdResolvedConfd := fmt.Sprintf(`# Generated by os-extension-metal +[Resolve] +DNS=%s +Domain=~. +`, resolveDNS) + resolvConf := "# Generated by os-extension-metal\n" + for _, ip := range dnsServers { + resolvConf += fmt.Sprintf("nameserver %s\n", ip) + } + + // TODO: in osc.Spec.Type we can get the distro "ubuntu", "debian", "nvidia", ... + // from this information we should be able to deduce if systemd-resolved is used or not + + return []extensionsv1alpha1.File{ + { + Path: "/etc/systemd/resolved.conf.d/dns.conf", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: systemdResolvedConfd, + }, + }, + }, + { + Path: "/etc/resolv.conf", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: resolvConf, + }, + }, + }, + } +} + +func additionalNTPConfFiles(ntpServers []string) []extensionsv1alpha1.File { + if len(ntpServers) == 0 { + return nil + } + ntps := strings.Join(ntpServers, " ") + renderedContent := fmt.Sprintf(`# Generated by os-extension-metal +[Time] +NTP=%s +`, ntps) + + return []extensionsv1alpha1.File{ + { + Path: "/etc/systemd/timesyncd.conf", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: renderedContent, + }, + }, + }, + } +} + +func EnsureFiles(base []extensionsv1alpha1.File, files ...extensionsv1alpha1.File) []extensionsv1alpha1.File { + var res []extensionsv1alpha1.File + + res = append(res, base...) + + for _, file := range files { + index := slices.IndexFunc(base, func(elem extensionsv1alpha1.File) bool { + return elem.Path == file.Path + }) + + if index < 0 { + res = append(res, file) + } else { + res[index] = file + } + } + + return res +} diff --git a/pkg/controller/operatingsystemconfig/actuator_test.go b/pkg/controller/operatingsystemconfig/actuator_test.go index f4c52b4..9d9825c 100644 --- a/pkg/controller/operatingsystemconfig/actuator_test.go +++ b/pkg/controller/operatingsystemconfig/actuator_test.go @@ -16,19 +16,22 @@ package operatingsystemconfig_test import ( "context" + _ "embed" + "encoding/json" "github.com/gardener/gardener/extensions/pkg/controller/operatingsystemconfig" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" "github.com/gardener/gardener/pkg/utils/test" "github.com/go-logr/logr" + metalextensionv1alpha1 "github.com/metal-stack/gardener-extension-provider-metal/pkg/apis/metal/v1alpha1" + . "github.com/metal-stack/os-metal-extension/pkg/controller/operatingsystemconfig" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/manager" - - . "github.com/metal-stack/os-metal-extension/pkg/controller/operatingsystemconfig" ) var _ = Describe("Actuator", func() { @@ -74,9 +77,161 @@ var _ = Describe("Actuator", func() { Expect(string(userData)).To(HaveSuffix("}")) // check we have ignition format Expect(command).To(BeNil()) Expect(unitNames).To(ConsistOf("some-unit.service")) - Expect(fileNames).To(ConsistOf("/some/file")) + Expect(fileNames).To(ConsistOf("/some/file", "/etc/containerd/config.toml")) + Expect(extensionUnits).To(BeEmpty()) + Expect(extensionFiles).To(HaveLen(1)) + }) + + It("network isolation files are added", func() { + osc = osc.DeepCopy() + osc.Spec.ProviderConfig = &runtime.RawExtension{ + Raw: mustMarshal(&metalextensionv1alpha1.ImageProviderConfig{ + NetworkIsolation: &metalextensionv1alpha1.NetworkIsolation{ + AllowedNetworks: metalextensionv1alpha1.AllowedNetworks{ + Ingress: []string{"10.0.0.1/24"}, + Egress: []string{"100.0.0.1/24"}, + }, + DNSServers: []string{"1.1.1.1", "1.0.0.1"}, + NTPServers: []string{"134.60.1.27", "134.60.111.110"}, + RegistryMirrors: []metalextensionv1alpha1.RegistryMirror{ + { + Name: "metal-stack registry", + Endpoint: "https://r.metal-stack.dev", + IP: "1.2.3.4", + Port: 443, + MirrorOf: []string{ + "ghcr.io", + "quay.io", + }, + }, + { + Name: "local registry", + Endpoint: "http://localhost:8080", + IP: "127.0.0.1", + Port: 8080, + MirrorOf: []string{ + "docker.io", + }, + }, + }, + }, + }), + } + + userData, command, unitNames, fileNames, extensionUnits, extensionFiles, err := actuator.Reconcile(ctx, log, osc) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(userData)).To(ContainSubstring("/etc/containerd/config.toml")) + Expect(string(userData)).To(HavePrefix("{")) // check we have ignition format + Expect(string(userData)).To(HaveSuffix("}")) // check we have ignition format + Expect(command).To(BeNil()) + Expect(unitNames).To(ConsistOf("some-unit.service")) + Expect(fileNames).To(ConsistOf( + "/some/file", + "/etc/containerd/config.toml", + "/etc/systemd/resolved.conf.d/dns.conf", + "/etc/resolv.conf", + "/etc/systemd/timesyncd.conf", + "/etc/containerd/certs.d/ghcr.io/hosts.toml", + "/etc/containerd/certs.d/quay.io/hosts.toml", + "/etc/containerd/certs.d/docker.io/hosts.toml", + )) Expect(extensionUnits).To(BeEmpty()) - Expect(extensionFiles).To(BeEmpty()) + Expect(extensionFiles).To(ConsistOf( + extensionsv1alpha1.File{ + Path: "/etc/systemd/resolved.conf.d/dns.conf", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: `# Generated by os-extension-metal +[Resolve] +DNS=1.1.1.1 1.0.0.1 +Domain=~. +`, + }, + }, + }, + extensionsv1alpha1.File{ + Path: "/etc/resolv.conf", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: `# Generated by os-extension-metal +nameserver 1.1.1.1 +nameserver 1.0.0.1 +`, + }, + }, + }, + extensionsv1alpha1.File{ + Path: "/etc/systemd/timesyncd.conf", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: `# Generated by os-extension-metal +[Time] +NTP=134.60.1.27 134.60.111.110 +`, + }, + }, + }, + extensionsv1alpha1.File{ + Path: "/etc/containerd/config.toml", + Permissions: ptr.To(int32(420)), + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: `# Generated by os-extension-metal +version = 2 +imports = ["/etc/containerd/conf.d/*.toml"] +disabled_plugins = [] + +[plugins."io.containerd.grpc.v1.cri".registry] + config_path = "/etc/containerd/certs.d" +`, + }, + }, + }, + extensionsv1alpha1.File{ + Path: "/etc/containerd/certs.d/ghcr.io/hosts.toml", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: `server = "https://ghcr.io" + +[host."https://r.metal-stack.dev"] + capabilities = ["pull", "resolve"] +`, + }, + }, + }, + extensionsv1alpha1.File{ + Path: "/etc/containerd/certs.d/quay.io/hosts.toml", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: `server = "https://quay.io" + +[host."https://r.metal-stack.dev"] + capabilities = ["pull", "resolve"] +`, + }, + }, + }, + extensionsv1alpha1.File{ + Path: "/etc/containerd/certs.d/docker.io/hosts.toml", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Encoding: string(extensionsv1alpha1.PlainFileCodecID), + Data: `server = "https://docker.io" + +[host."http://localhost:8080"] + capabilities = ["pull", "resolve"] +`, + }, + }, + }, + )) }) }) }) @@ -94,9 +249,71 @@ var _ = Describe("Actuator", func() { Expect(userData).NotTo(BeEmpty()) // legacy logic is tested in ./generator/generator_test.go Expect(command).To(BeNil()) Expect(unitNames).To(ConsistOf("some-unit.service")) - Expect(fileNames).To(ConsistOf("/some/file")) + Expect(fileNames).To(ConsistOf("/some/file", "/etc/containerd/config.toml")) + }) + }) + }) + }) + + When("EnsureFiles", func() { + Describe("Ensures files", func() { + var ( + testFile1 = extensionsv1alpha1.File{ + Path: "/etc/foo", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Data: "foo", + }, + }, + } + testFile2 = extensionsv1alpha1.File{ + Path: "/etc/bar", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Data: "bar", + }, + }, + } + testFile3 = extensionsv1alpha1.File{ + Path: "/etc/bar", + Content: extensionsv1alpha1.FileContent{ + Inline: &extensionsv1alpha1.FileContentInline{ + Data: "bar different", + }, + }, + } + ) + + It("Ensures a single file into empty base", func() { + result := EnsureFiles([]extensionsv1alpha1.File{}, testFile1) + Expect(result).To(ConsistOf(testFile1)) + }) + + It("Ensures no file into non-empty base", func() { + result := EnsureFiles([]extensionsv1alpha1.File{ + testFile2, }) + Expect(result).To(ConsistOf(testFile2)) + }) + + It("Ensures a single file into non-empty base", func() { + result := EnsureFiles([]extensionsv1alpha1.File{ + testFile2, + }, testFile1) + Expect(result).To(ConsistOf(testFile2, testFile1)) + }) + + It("Ensures only single file is added", func() { + result := EnsureFiles([]extensionsv1alpha1.File{ + testFile2, + }, testFile3) + Expect(result).To(ConsistOf(testFile3)) }) }) }) }) + +func mustMarshal(data any) []byte { + raw, _ := json.Marshal(data) //nolint + return raw +} diff --git a/pkg/controller/operatingsystemconfig/generator/ignition/generator.go b/pkg/controller/operatingsystemconfig/generator/ignition/generator.go index f5e62c1..f2ce066 100644 --- a/pkg/controller/operatingsystemconfig/generator/ignition/generator.go +++ b/pkg/controller/operatingsystemconfig/generator/ignition/generator.go @@ -3,33 +3,16 @@ package ignition import ( "encoding/json" "fmt" - "strings" "text/template" - gardenv1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" - metalextensionv1alpha1 "github.com/metal-stack/gardener-extension-provider-metal/pkg/apis/metal/v1alpha1" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "github.com/flatcar/container-linux-config-transpiler/config/types" "github.com/gardener/gardener/extensions/pkg/controller/operatingsystemconfig/oscommon/generator" ostemplate "github.com/gardener/gardener/extensions/pkg/controller/operatingsystemconfig/oscommon/template" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/utils/ptr" ) -const ( - containerdConfig = `# Generated by os-extension-metal -imports = ["/etc/containerd/conf.d/*.toml"] -disabled_plugins = [] - -[plugins."io.containerd.grpc.v1.cri".registry] - config_path = "/etc/containerd/certs.d" -` -) - // IgnitionGenerator generates cloud-init scripts. type IgnitionGenerator struct { cloudInitGenerator generator.Generator @@ -56,7 +39,18 @@ func (t *IgnitionGenerator) Generate(logr logr.Logger, config *generator.Operati cmd = &c } - data, err := ignitionFromOperatingSystemConfig(config) + cfg := ignitionFromOperatingSystemConfig(config) + + outCfg, report := types.Convert(cfg, "", nil) + if report.IsFatal() { + return nil, nil, fmt.Errorf("could not transpile ignition config: %s", report.String()) + } + + data, err := json.Marshal(outCfg) + if err != nil { + return nil, nil, err + } + return data, cmd, err } @@ -67,21 +61,9 @@ func (t *IgnitionGenerator) Generate(logr logr.Logger, config *generator.Operati // Starting with ignition 2.0, ignition itself contains the required parsing logic, so we can use ignition directly. // see https://github.com/coreos/ignition/blob/master/config/config.go#L38 // Therefore we must update ignition to 2.0.0 in the images and transform the gardener config to the ignition config types instead. -func ignitionFromOperatingSystemConfig(config *generator.OperatingSystemConfig) ([]byte, error) { +func ignitionFromOperatingSystemConfig(config *generator.OperatingSystemConfig) types.Config { cfg := types.Config{} - imageProviderConfig := &metalextensionv1alpha1.ImageProviderConfig{} - networkIsolation := &metalextensionv1alpha1.NetworkIsolation{} - if config.Object != nil && config.Object.Spec.ProviderConfig != nil { - err := decodeProviderConfig(config.Object.Spec.ProviderConfig, imageProviderConfig) - if err != nil { - return nil, fmt.Errorf("unable to decode providerConfig") - } - } - if imageProviderConfig.NetworkIsolation != nil { - networkIsolation = imageProviderConfig.NetworkIsolation - } - cfg.Systemd = types.Systemd{} for _, u := range config.Units { contents := string(u.Content) @@ -115,147 +97,10 @@ func ignitionFromOperatingSystemConfig(config *generator.OperatingSystemConfig) Contents: types.FileContents{ Inline: string(f.Content), }, - } - cfg.Storage.Files = append(cfg.Storage.Files, ignitionFile) - } - - dnsFiles := additionalDNSConfFiles(networkIsolation.DNSServers) - cfg.Storage.Files = append(cfg.Storage.Files, dnsFiles...) - - ntpFiles := additionalNTPConfFiles(networkIsolation.NTPServers) - cfg.Storage.Files = append(cfg.Storage.Files, ntpFiles...) - - if config.CRI != nil && config.CRI.Name == extensionsv1alpha1.CRINameContainerD { - // the debian:12 containerd ships with "cri" plugin disabled, so we need override the config that ships with the os - // - // with g/g v1.100 it would be best to just remove the config.toml and let the GNA generate the default config. - // unfortunately, ignition does not allow to remove files easily. - // along with the default import paths. see https://github.com/gardener/gardener/pull/10050) - cfg.Storage.Files = append(cfg.Storage.Files, types.File{ - Path: "/etc/containerd/config.toml", - Filesystem: "root", - Contents: types.FileContents{ - Inline: containerdConfig, - }, Overwrite: ptr.To(true), - }) - - if len(networkIsolation.RegistryMirrors) > 0 { - cfg.Storage.Files = append(cfg.Storage.Files, additionalContainerdMirrors(networkIsolation.RegistryMirrors)) } + cfg.Storage.Files = append(cfg.Storage.Files, ignitionFile) } - outCfg, report := types.Convert(cfg, "", nil) - if report.IsFatal() { - return nil, fmt.Errorf("could not transpile ignition config: %s", report.String()) - } - - return json.Marshal(outCfg) -} - -// decodeProviderConfig decodes the provider config into the given struct -func decodeProviderConfig(providerConfig *runtime.RawExtension, into runtime.Object) error { - if providerConfig == nil { - return nil - } - - if _, _, err := getGardenerDecoder().Decode(providerConfig.Raw, nil, into); err != nil { - return fmt.Errorf("could not decode provider config: %w", err) - } - - return nil -} - -var ( - decoder runtime.Decoder -) - -// getGardenerDecoder returns a decoder to decode Gardener objects -func getGardenerDecoder() runtime.Decoder { - if decoder == nil { - scheme := runtime.NewScheme() - utilruntime.Must(gardenv1beta1.AddToScheme(scheme)) - decoder = serializer.NewCodecFactory(scheme).UniversalDecoder() - } - return decoder -} - -func additionalContainerdMirrors(mirrors []metalextensionv1alpha1.RegistryMirror) types.File { - content := `# Generated by os-extension-metal -version = 2 - -[plugins."io.containerd.grpc.v1.cri".registry] - [plugins."io.containerd.grpc.v1.cri".registry.mirrors] -` - for _, m := range mirrors { - for _, of := range m.MirrorOf { - content += fmt.Sprintf(` [plugins."io.containerd.grpc.v1.cri".registry.mirrors.%q] - endpoint = [%q] -`, of, m.Endpoint) - } - } - - return types.File{ - Path: "/etc/containerd/conf.d/isolated-cluster.toml", - Filesystem: "root", - Mode: &types.DefaultFileMode, - Contents: types.FileContents{ - Inline: content, - }, - } - -} - -func additionalDNSConfFiles(dnsServers []string) []types.File { - if len(dnsServers) == 0 { - return nil - } - resolveDNS := strings.Join(dnsServers, " ") - systemdResolvedConfd := fmt.Sprintf(`# Generated by os-extension-metal - -[Resolve] -DNS=%s -Domain=~. - -`, resolveDNS) - resolvConf := "# Generated by os-extension-metal\n" - for _, ip := range dnsServers { - resolvConf += fmt.Sprintf("nameserver %s\n", ip) - } - - return []types.File{ - { - Path: "/etc/systemd/resolved.conf.d/dns.conf", - Contents: types.FileContents{ - Inline: systemdResolvedConfd, - }, - }, - { - Path: "/etc/resolv.conf", - Contents: types.FileContents{ - Inline: resolvConf, - }, - }, - } -} - -func additionalNTPConfFiles(ntpServers []string) []types.File { - if len(ntpServers) == 0 { - return nil - } - ntps := strings.Join(ntpServers, " ") - renderedContent := fmt.Sprintf(`# Generated by os-extension-metal - -[Time] -NTP=%s -`, ntps) - - return []types.File{ - { - Path: "/etc/systemd/timesyncd.conf", - Contents: types.FileContents{ - Inline: renderedContent, - }, - }, - } + return cfg } diff --git a/pkg/controller/operatingsystemconfig/generator/ignition/generator_test.go b/pkg/controller/operatingsystemconfig/generator/ignition/generator_test.go index 4c17fed..a1263f1 100644 --- a/pkg/controller/operatingsystemconfig/generator/ignition/generator_test.go +++ b/pkg/controller/operatingsystemconfig/generator/ignition/generator_test.go @@ -1,24 +1,21 @@ package ignition import ( - "encoding/json" - "reflect" "testing" "github.com/flatcar/container-linux-config-transpiler/config/types" "github.com/gardener/gardener/extensions/pkg/controller/operatingsystemconfig/oscommon/generator" - extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" - metalextensionv1alpha1 "github.com/metal-stack/gardener-extension-provider-metal/pkg/apis/metal/v1alpha1" - "k8s.io/apimachinery/pkg/runtime" + "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + "github.com/go-logr/logr" + "github.com/google/go-cmp/cmp" "k8s.io/utils/ptr" ) func TestIgnitionFromOperatingSystemConfig(t *testing.T) { tests := []struct { - name string - config *generator.OperatingSystemConfig - want types.Config - wantErr bool + name string + config *generator.OperatingSystemConfig + want types.Config }{ { name: "simple service", @@ -26,11 +23,10 @@ func TestIgnitionFromOperatingSystemConfig(t *testing.T) { Units: []*generator.Unit{ { Name: "kubelet.service", - Content: []byte(("[Unit]\nDescription=kubelet\n[Install]\nWantedBy=multi-user.target\n[Service]\nExecStart=/bin/kubelet")), + Content: []byte("[Unit]\nDescription=kubelet\n[Install]\nWantedBy=multi-user.target\n[Service]\nExecStart=/bin/kubelet"), }, }, }, - wantErr: false, want: types.Config{ Systemd: types.Systemd{ Units: []types.SystemdUnit{ @@ -43,20 +39,22 @@ func TestIgnitionFromOperatingSystemConfig(t *testing.T) { }, }, }, - { name: "simple files", config: &generator.OperatingSystemConfig{ Files: []*generator.File{ { - Path: "/etc/hostname", - TransmitUnencoded: ptr.To(true), - Content: []byte("testhost"), - Permissions: ptr.To(int32(0644)), + Path: "/etc/hostname", + Content: []byte("testhost"), + Permissions: ptr.To(int32(0644)), + }, + { + Path: "/etc/foo", + Content: []byte("foo"), + Permissions: ptr.To(int32(0744)), }, }, }, - wantErr: false, want: types.Config{ Storage: types.Storage{ Files: []types.File{ @@ -64,122 +62,19 @@ func TestIgnitionFromOperatingSystemConfig(t *testing.T) { Filesystem: "root", Path: "/etc/hostname", Contents: types.FileContents{ - // FIXME here should be testhosts ??? Inline: "testhost", }, - Mode: ptr.To(0644), - }, - }, - }, - }, - }, - { - name: "containerd with network isolation", - config: &generator.OperatingSystemConfig{ - CRI: &extensionsv1alpha1.CRIConfig{ - Name: extensionsv1alpha1.CRINameContainerD, - }, - Object: &extensionsv1alpha1.OperatingSystemConfig{ - Spec: extensionsv1alpha1.OperatingSystemConfigSpec{ - DefaultSpec: extensionsv1alpha1.DefaultSpec{ - ProviderConfig: &runtime.RawExtension{ - Raw: mustMarshal(t, &metalextensionv1alpha1.ImageProviderConfig{ - NetworkIsolation: &metalextensionv1alpha1.NetworkIsolation{ - AllowedNetworks: metalextensionv1alpha1.AllowedNetworks{ - Ingress: []string{"10.0.0.1/24"}, - Egress: []string{"100.0.0.1/24"}, - }, - DNSServers: []string{"1.1.1.1", "1.0.0.1"}, - NTPServers: []string{"134.60.1.27", "134.60.111.110"}, - RegistryMirrors: []metalextensionv1alpha1.RegistryMirror{ - { - Name: "metal-stack registry", - Endpoint: "https://r.metal-stack.dev", - IP: "1.2.3.4", - Port: 443, - MirrorOf: []string{ - "ghcr.io", - "quay.io", - }, - }, - { - Name: "local registry", - Endpoint: "http://localhost:8080", - IP: "127.0.0.1", - Port: 8080, - MirrorOf: []string{ - "docker.io", - }, - }, - }, - }}), - }, - }, - }, - }, - }, - wantErr: false, - want: types.Config{ - Storage: types.Storage{ - Files: []types.File{ - { - Path: "/etc/systemd/resolved.conf.d/dns.conf", - Contents: types.FileContents{ - Inline: `# Generated by os-extension-metal - -[Resolve] -DNS=1.1.1.1 1.0.0.1 -Domain=~. - -`, - }, - }, - { - Path: "/etc/resolv.conf", - Contents: types.FileContents{ - Inline: `# Generated by os-extension-metal -nameserver 1.1.1.1 -nameserver 1.0.0.1 -`, - }, - }, - { - Path: "/etc/systemd/timesyncd.conf", - Contents: types.FileContents{ - Inline: `# Generated by os-extension-metal - -[Time] -NTP=134.60.1.27 134.60.111.110 -`, - }, - }, - { - Filesystem: "root", - Path: "/etc/containerd/config.toml", - Mode: &types.DefaultFileMode, - Contents: types.FileContents{ - Inline: containerdConfig, - }, + Mode: ptr.To(0644), Overwrite: ptr.To(true), }, { Filesystem: "root", - Path: "/etc/containerd/conf.d/isolated-cluster.toml", - Mode: &types.DefaultFileMode, + Path: "/etc/foo", Contents: types.FileContents{ - Inline: `# Generated by os-extension-metal -version = 2 - -[plugins."io.containerd.grpc.v1.cri".registry] - [plugins."io.containerd.grpc.v1.cri".registry.mirrors] - [plugins."io.containerd.grpc.v1.cri".registry.mirrors."ghcr.io"] - endpoint = ["https://r.metal-stack.dev"] - [plugins."io.containerd.grpc.v1.cri".registry.mirrors."quay.io"] - endpoint = ["https://r.metal-stack.dev"] - [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] - endpoint = ["http://localhost:8080"] -`, + Inline: "foo", }, + Mode: ptr.To(0744), + Overwrite: ptr.To(true), }, }, }, @@ -189,28 +84,50 @@ version = 2 for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - got, err := ignitionFromOperatingSystemConfig(tt.config) - if (err != nil) != tt.wantErr { - t.Errorf("ignitionFromOperatingSystemConfig() error = %v, wantErr %v", err, tt.wantErr) - return - } - - cfg, _ := types.Convert(tt.want, "", nil) - want, err := json.Marshal(cfg) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("ignitionFromOperatingSystemConfig()\ngot:\n%v\nwant:\n%v", string(got), string(want)) + got := ignitionFromOperatingSystemConfig(tt.config) + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Errorf("diff = %s", diff) } }) } } -func mustMarshal(t *testing.T, obj runtime.Object) []byte { - data, err := json.Marshal(obj) - if err != nil { - t.Errorf("failed to marshal object %s", err) +func Test_ignition_Transpile(t *testing.T) { + tests := []struct { + name string + osc *generator.OperatingSystemConfig + want string + wantErr bool + }{ + { + name: "transpiles to ignition format 2.3.0", + osc: &generator.OperatingSystemConfig{ + Object: &v1alpha1.OperatingSystemConfig{ + Spec: v1alpha1.OperatingSystemConfigSpec{ + Purpose: v1alpha1.OperatingSystemConfigPurposeProvision, + }, + }, + Files: []*generator.File{ + { + Path: "/etc/a", + }, + }, + }, + want: `{"ignition":{"config":{},"security":{"tls":{}},"timeouts":{},"version":"2.3.0"},"networkd":{},"passwd":{},"storage":{"files":[{"filesystem":"root","overwrite":true,"path":"/etc/a","contents":{"source":"data:,","verification":{}},"mode":420}]},"systemd":{}}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &IgnitionGenerator{} + + got, _, err := tr.Generate(logr.Discard(), tt.osc) + if (err != nil) != tt.wantErr { + t.Errorf("error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(string(got), tt.want); diff != "" { + t.Errorf("diff = %s", diff) + } + }) } - return data }