Skip to content
Open
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
5 changes: 5 additions & 0 deletions cmd/generate-config/config/config-openapi-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,11 @@
"default": "example.com",
"example": "microshift.example.com"
},
"configFile": {
"description": "configFile is the path to a custom CoreDNS Corefile on the host filesystem.\nWhen set, MicroShift uses this file as the Corefile in the dns-default ConfigMap,\nfully replacing the default template-rendered configuration.\nChanges to this file are detected at runtime and applied without restarting MicroShift.\nMutually exclusive with dns.hosts: setting both causes a startup error.",
"type": "string",
"example": "/etc/microshift/dns/Corefile"
},
"hosts": {
"description": "Hosts contains configuration for the hosts file.",
"type": "object",
Expand Down
2 changes: 2 additions & 0 deletions docs/user/howto_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ debugging:
logLevel: ""
dns:
baseDomain: ""
configFile: ""
hosts:
file: ""
status: ""
Expand Down Expand Up @@ -198,6 +199,7 @@ debugging:
logLevel: Normal
dns:
baseDomain: example.com
configFile: ""
hosts:
file: /etc/hosts
status: Disabled
Expand Down
8 changes: 8 additions & 0 deletions packaging/microshift/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ dns:
# example:
# microshift.example.com
baseDomain: example.com
# configFile is the path to a custom CoreDNS Corefile on the host filesystem.
# When set, MicroShift uses this file as the Corefile in the dns-default ConfigMap,
# fully replacing the default template-rendered configuration.
# Changes to this file are detected at runtime and applied without restarting MicroShift.
# Mutually exclusive with dns.hosts: setting both causes a startup error.
# example:
# /etc/microshift/dns/Corefile
configFile: ""
# Hosts contains configuration for the hosts file.
hosts:
# File is the path to the hosts file to monitor.
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ func RunMicroshift(cfg *config.Config) error {
util.Must(m.AddService(controllers.NewClusterID(cfg)))
util.Must(m.AddService(controllers.NewTelemetryManager(cfg)))
util.Must(m.AddService(controllers.NewHostsWatcherManager(cfg)))
util.Must(m.AddService(controllers.NewDNSConfigurationWatcherManager(cfg)))
util.Must(m.AddService(gdp.NewGenericDevicePlugin(cfg)))

// Storing and clearing the env, so other components don't send the READY=1 until MicroShift is fully ready
Expand Down
30 changes: 22 additions & 8 deletions pkg/components/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,9 @@ func startDNSController(ctx context.Context, cfg *config.Config, kubeconfigPath
"components/openshift-dns/dns/service-account.yaml",
"components/openshift-dns/node-resolver/service-account.yaml",
}
cm = []string{
"components/openshift-dns/dns/configmap.yaml",
}
svc = []string{
cm = "components/openshift-dns/dns/configmap.yaml"
cmList = []string{cm}
svc = []string{
"components/openshift-dns/dns/service.yaml",
}
)
Expand All @@ -303,9 +302,10 @@ func startDNSController(ctx context.Context, cfg *config.Config, kubeconfigPath
return err
}

hostsEnabled := cfg.DNS.Hosts.Status == config.HostsStatusEnabled
extraParams := assets.RenderParams{
"ClusterIP": cfg.Network.DNS,
"HostsEnabled": cfg.DNS.Hosts.Status == config.HostsStatusEnabled,
"HostsEnabled": hostsEnabled,
}

if err := assets.ApplyServices(ctx, svc, renderTemplate, renderParamsFromConfig(cfg, extraParams), kubeconfigPath); err != nil {
Expand Down Expand Up @@ -333,10 +333,24 @@ func startDNSController(ctx context.Context, cfg *config.Config, kubeconfigPath
klog.Warningf("Failed to apply serviceAccount %v %v", sa, err)
return err
}
if err := assets.ApplyConfigMaps(ctx, cm, renderTemplate, renderParamsFromConfig(cfg, extraParams), kubeconfigPath); err != nil {
klog.Warningf("Failed to apply configMap %v %v", cm, err)
return err

if cfg.DNS.ConfigFile != "" {
corefileContent, err := os.ReadFile(cfg.DNS.ConfigFile)
if err != nil {
klog.Warningf("Failed to read custom DNS config file %s: %v", cfg.DNS.ConfigFile, err)
return err
}
if err := assets.ApplyConfigMapWithData(ctx, cm, map[string]string{"Corefile": string(corefileContent)}, kubeconfigPath); err != nil {
klog.Warningf("Failed to apply custom DNS configMap: %v", err)
return err
}
Comment thread
pacevedom marked this conversation as resolved.
} else {
if err := assets.ApplyConfigMaps(ctx, cmList, renderTemplate, renderParamsFromConfig(cfg, extraParams), kubeconfigPath); err != nil {
klog.Warningf("Failed to apply configMap %v %v", cmList, err)
return err
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if err := assets.ApplyDaemonSets(ctx, apps, renderTemplate, renderParamsFromConfig(cfg, extraParams), kubeconfigPath); err != nil {
klog.Warningf("Failed to apply apps %v %v", apps, err)
return err
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ func (c *Config) incorporateUserSettings(u *Config) {
if u.DNS.BaseDomain != "" {
c.DNS.BaseDomain = u.DNS.BaseDomain
}
if u.DNS.ConfigFile != "" {
c.DNS.ConfigFile = u.DNS.ConfigFile
}

if u.Network.CNIPlugin != "" {
c.Network.CNIPlugin = u.Network.CNIPlugin
Expand Down
85 changes: 60 additions & 25 deletions pkg/config/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ type DNS struct {
// +kubebuilder:example=microshift.example.com
BaseDomain string `json:"baseDomain"`

// configFile is the path to a custom CoreDNS Corefile on the host filesystem.
// When set, MicroShift uses this file as the Corefile in the dns-default ConfigMap,
// fully replacing the default template-rendered configuration.
// Changes to this file are detected at runtime and applied without restarting MicroShift.
// Mutually exclusive with dns.hosts: setting both causes a startup error.
// +optional
// +kubebuilder:example="/etc/microshift/dns/Corefile"
ConfigFile string `json:"configFile,omitempty"`

// Hosts contains configuration for the hosts file.
Hosts HostsConfig `json:"hosts,omitempty"`
}
Expand Down Expand Up @@ -59,40 +68,66 @@ func dnsDefaults() DNS {
}

func (t *DNS) validate() error {
switch t.Hosts.Status {
case HostsStatusEnabled:
if t.Hosts.File == "" {
break
}
if t.ConfigFile != "" && t.Hosts.Status == HostsStatusEnabled {
return fmt.Errorf("dns.configFile and dns.hosts are mutually exclusive")
}

cleanPath := filepath.Clean(t.Hosts.File)
if err := t.validateConfigFile(); err != nil {
return err
}

fi, err := os.Stat(cleanPath)
// Enforce ConfigMap requirement: the file must not exceed 1MiB, as it will be mounted into a ConfigMap.
if err == nil && fi.Size() > 1048576 {
return fmt.Errorf("hosts file %s exceeds 1MiB ConfigMap (and internal buffer) size limit (got %d bytes)", t.Hosts.File, fi.Size())
}
if !filepath.IsAbs(cleanPath) {
return fmt.Errorf("hosts file path must be absolute: got %s", t.Hosts.File)
}
return t.validateHosts()
}

_, err = os.Stat(cleanPath)
if os.IsNotExist(err) {
return fmt.Errorf("hosts file %s does not exist", t.Hosts.File)
} else if err != nil {
return fmt.Errorf("error checking hosts file %s: %v", t.Hosts.File, err)
}
func (t *DNS) validateConfigFile() error {
if t.ConfigFile == "" {
return nil
}
return validateFilePath(t.ConfigFile, "dns config file")
}

file, err := os.Open(t.Hosts.File)
if err != nil {
return fmt.Errorf("hosts file %s is not readable: %v", t.Hosts.File, err)
func (t *DNS) validateHosts() error {
switch t.Hosts.Status {
case HostsStatusEnabled:
if t.Hosts.File == "" {
break
}
return file.Close()

return validateFilePath(t.Hosts.File, "hosts file")
case HostsStatusDisabled:
return nil
default:
return fmt.Errorf("invalid hosts status: %s", t.Hosts.Status)
}
return nil
}

func validateFilePath(path, label string) error {
cleanPath := filepath.Clean(path)
if !filepath.IsAbs(cleanPath) {
return fmt.Errorf("%s path must be absolute: got %s", label, path)
}

fi, err := os.Stat(cleanPath)
if os.IsNotExist(err) {
return fmt.Errorf("%s %s does not exist", label, path)
} else if err != nil {
return fmt.Errorf("error checking %s %s: %v", label, path, err)
}
if !fi.Mode().IsRegular() {
return fmt.Errorf("%s %s must be a regular file", label, path)
}

if fi.Size() == 0 {
return fmt.Errorf("%s %s is empty", label, path)
}
Comment thread
pacevedom marked this conversation as resolved.

if fi.Size() > 1048576 {
return fmt.Errorf("%s %s exceeds 1MiB size limit (got %d bytes)", label, path, fi.Size())
}

file, err := os.Open(cleanPath)
if err != nil {
return fmt.Errorf("%s %s is not readable: %v", label, path, err)
}
return file.Close()
Comment thread
pacevedom marked this conversation as resolved.
}
141 changes: 141 additions & 0 deletions pkg/config/dns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package config

import (
"fmt"
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDNS_ValidateConfigFile_MutualExclusivity(t *testing.T) {
tmpFile := createTempCorefile(t, ".:5353 { whoami }")

dns := DNS{
ConfigFile: tmpFile,
Hosts: HostsConfig{
Status: HostsStatusEnabled,
File: "/etc/hosts",
},
}
err := dns.validate()
assert.ErrorContains(t, err, "dns.configFile and dns.hosts are mutually exclusive")
}

func TestDNS_ValidateConfigFile_WithHostsDisabled(t *testing.T) {
tmpFile := createTempCorefile(t, ".:5353 { whoami }")

dns := DNS{
ConfigFile: tmpFile,
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
assert.NoError(t, dns.validate())
}

func TestDNS_ValidateConfigFile_EmptyConfigFilePreservesDefault(t *testing.T) {
dns := DNS{
ConfigFile: "",
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
assert.NoError(t, dns.validate())
}

func TestDNS_ValidateConfigFile_NonAbsolutePath(t *testing.T) {
dns := DNS{
ConfigFile: "relative/path/Corefile",
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
err := dns.validate()
assert.ErrorContains(t, err, "dns config file path must be absolute")
}

func TestDNS_ValidateConfigFile_NonExistentFile(t *testing.T) {
dns := DNS{
ConfigFile: "/tmp/nonexistent-corefile-test-12345",
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
err := dns.validate()
assert.ErrorContains(t, err, "does not exist")
}

func TestDNS_ValidateConfigFile_Directory(t *testing.T) {
tmpDir := t.TempDir()

dns := DNS{
ConfigFile: tmpDir,
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
err := dns.validate()
assert.ErrorContains(t, err, "must be a regular file")
}

func TestDNS_ValidateConfigFile_EmptyFile(t *testing.T) {
tmpFile := createTempCorefile(t, "")

dns := DNS{
ConfigFile: tmpFile,
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
err := dns.validate()
assert.ErrorContains(t, err, "is empty")
}

func TestDNS_ValidateConfigFile_ExceedsSizeLimit(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "Corefile")
// 1 MiB + 1 byte
data := make([]byte, 1048576+1)
require.NoError(t, os.WriteFile(tmpFile, data, 0644))

dns := DNS{
ConfigFile: tmpFile,
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
err := dns.validate()
assert.ErrorContains(t, err, "exceeds 1MiB size limit")
}

func TestDNS_ValidateConfigFile_ValidFile(t *testing.T) {
tmpFile := createTempCorefile(t, ".:5353 {\n whoami\n reload\n}\n")

dns := DNS{
ConfigFile: tmpFile,
Hosts: HostsConfig{
Status: HostsStatusDisabled,
},
}
assert.NoError(t, dns.validate())
}

func TestDNS_ConfigFile_IncorporatedFromDropIn(t *testing.T) {
tmpFile := createTempCorefile(t, ".:5353 {\n whoami\n reload\n}\n")

yamlConfig := fmt.Sprintf("dns:\n configFile: %s\n", tmpFile)
config, err := getActiveConfigFromYAMLDropins([][]byte{[]byte(yamlConfig)})
require.NoError(t, err)
assert.Equal(t, tmpFile, config.DNS.ConfigFile)
}

func createTempCorefile(t *testing.T, content string) string {
t.Helper()
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "Corefile")
require.NoError(t, os.WriteFile(tmpFile, []byte(content), 0644))
return tmpFile
}
33 changes: 33 additions & 0 deletions pkg/controllers/dnsconfigurationwatcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package controllers
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is very very similar to hostswatcher.go... If we don't refactor them now, we'll never do it


import (
"github.com/fsnotify/fsnotify"
"github.com/openshift/microshift/pkg/config"
)

type DNSConfigurationWatcherManager struct {
fileWatcher
}

func NewDNSConfigurationWatcherManager(cfg *config.Config) *DNSConfigurationWatcherManager {
return &DNSConfigurationWatcherManager{fileWatcher{cfg: fileWatcherConfig{
serviceName: "dns-configuration-watcher-manager",
dependencies: []string{"infrastructure-services-manager"},
file: cfg.DNS.ConfigFile,
kubeconfig: cfg.KubeConfigPath(config.KubeAdmin),
enabled: cfg.DNS.ConfigFile != "",
configMapNamespace: "openshift-dns",
configMapName: "dns-default",
configMapDataKey: "Corefile",
labels: map[string]string{
"dns.operator.openshift.io/owning-dns": "default",
},
annotations: map[string]string{
"microshift.io/dns-config-file": cfg.DNS.ConfigFile,
},
eventMask: fsnotify.Write | fsnotify.Create | fsnotify.Rename | fsnotify.Remove,
reAddOnCreate: true,
mergeAnnotations: true,
deleteOnDisable: false,
}}}
}
Loading