feat: implement EtcFileController to render files in /etc

This implements two controllers: one which generates templates for
`/etc/hosts` and `/etc/resolv.config`, and another generic controller
which renders files to `/etc` via shadow bind mounts.

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
This commit is contained in:
Andrey Smirnov 2021-06-04 21:15:02 +03:00 committed by talos-bot
parent 5aede1a833
commit e74f789b01
11 changed files with 1005 additions and 1 deletions

View File

@ -0,0 +1,186 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package files
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"go.uber.org/zap"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/pkg/resources/files"
)
// EtcFileController watches EtcFileSpecs, creates/updates files.
type EtcFileController struct {
// Path to /etc directory, read-only filesystem.
EtcPath string
// Shadow path where actual file will be created and bind mounted into EtcdPath.
ShadowPath string
// Cache of bind mounts created.
bindMounts map[string]interface{}
}
// Name implements controller.Controller interface.
func (ctrl *EtcFileController) Name() string {
return "files.EtcFileController"
}
// Inputs implements controller.Controller interface.
func (ctrl *EtcFileController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: files.NamespaceName,
Type: files.EtcFileSpecType,
Kind: controller.InputStrong,
},
}
}
// Outputs implements controller.Controller interface.
func (ctrl *EtcFileController) Outputs() []controller.Output {
return []controller.Output{
{
Type: files.EtcFileStatusType,
Kind: controller.OutputExclusive,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo,cyclop
func (ctrl *EtcFileController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
if ctrl.bindMounts == nil {
ctrl.bindMounts = make(map[string]interface{})
}
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}
list, err := r.List(ctx, resource.NewMetadata(files.NamespaceName, files.EtcFileSpecType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing specs: %w", err)
}
// add finalizers for all live resources
for _, res := range list.Items {
if res.Metadata().Phase() != resource.PhaseRunning {
continue
}
if err = r.AddFinalizer(ctx, res.Metadata(), ctrl.Name()); err != nil {
return fmt.Errorf("error adding finalizer: %w", err)
}
}
touchedIDs := make(map[resource.ID]struct{})
for _, item := range list.Items {
spec := item.(*files.EtcFileSpec) //nolint:errcheck,forcetypeassert
filename := spec.Metadata().ID()
_, mountExists := ctrl.bindMounts[filename]
src := filepath.Join(ctrl.ShadowPath, filename)
dst := filepath.Join(ctrl.EtcPath, filename)
switch spec.Metadata().Phase() {
case resource.PhaseTearingDown:
if mountExists {
logger.Debug("removing bind mount", zap.String("src", src), zap.String("dst", dst))
if err = unix.Unmount(dst, 0); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to unmount bind mount %q: %w", dst, err)
}
delete(ctrl.bindMounts, filename)
}
logger.Debug("removing file", zap.String("src", src))
if err = os.Remove(src); err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to remove %q: %w", src, err)
}
// now remove finalizer as the link was deleted
if err = r.RemoveFinalizer(ctx, spec.Metadata(), ctrl.Name()); err != nil {
return fmt.Errorf("error removing finalizer: %w", err)
}
case resource.PhaseRunning:
if !mountExists {
logger.Debug("creating bind mount", zap.String("src", src), zap.String("dst", dst))
if err = createBindMount(src, dst); err != nil {
return fmt.Errorf("failed to create shadow bind mount %q -> %q: %w", src, dst, err)
}
ctrl.bindMounts[filename] = struct{}{}
}
logger.Debug("writing file contents", zap.String("dst", dst), zap.Stringer("version", spec.Metadata().Version()))
if err = os.WriteFile(dst, spec.TypedSpec().Contents, spec.TypedSpec().Mode); err != nil {
return fmt.Errorf("error updating %q: %w", dst, err)
}
if err = r.Modify(ctx, files.NewEtcFileStatus(files.NamespaceName, filename), func(r resource.Resource) error {
r.(*files.EtcFileStatus).TypedSpec().SpecVersion = spec.Metadata().Version().String()
return nil
}); err != nil {
return fmt.Errorf("error updating status: %w", err)
}
touchedIDs[filename] = struct{}{}
}
}
// list statuses for cleanup
list, err = r.List(ctx, resource.NewMetadata(files.NamespaceName, files.EtcFileStatusType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error listing resources: %w", err)
}
for _, res := range list.Items {
if _, ok := touchedIDs[res.Metadata().ID()]; !ok {
if err = r.Destroy(ctx, res.Metadata()); err != nil {
return fmt.Errorf("error cleaning up specs: %w", err)
}
}
}
}
}
// createBindMount creates a common way to create a writable source file with a
// bind mounted destination. This is most commonly used for well known files
// under /etc that need to be adjusted during startup.
func createBindMount(src, dst string) (err error) {
var f *os.File
if f, err = os.OpenFile(src, os.O_WRONLY|os.O_CREATE, 0o644); err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
if err = unix.Mount(src, dst, "", unix.MS_BIND, ""); err != nil {
return fmt.Errorf("failed to create bind mount for %s: %w", dst, err)
}
return nil
}

View File

@ -0,0 +1,142 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package files_test
import (
"context"
"log"
"os"
"path/filepath"
"strconv"
"sync"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/controller/runtime"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/go-retry/retry"
filesctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/files"
"github.com/talos-systems/talos/pkg/logging"
"github.com/talos-systems/talos/pkg/resources/files"
)
type EtcFileSuite struct {
suite.Suite
state state.State
runtime *runtime.Runtime
wg sync.WaitGroup
ctx context.Context
ctxCancel context.CancelFunc
etcPath string
shadowPath string
}
func (suite *EtcFileSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)
suite.state = state.WrapCore(namespaced.NewState(inmem.Build))
var err error
suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer()))
suite.Require().NoError(err)
suite.startRuntime()
suite.etcPath = suite.T().TempDir()
suite.shadowPath = suite.T().TempDir()
suite.Require().NoError(suite.runtime.RegisterController(&filesctrl.EtcFileController{
EtcPath: suite.etcPath,
ShadowPath: suite.shadowPath,
}))
}
func (suite *EtcFileSuite) startRuntime() {
suite.wg.Add(1)
go func() {
defer suite.wg.Done()
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
}()
}
func (suite *EtcFileSuite) assertEtcFile(filename, contents string, expectedVersion resource.Version) error {
b, err := os.ReadFile(filepath.Join(suite.etcPath, filename))
if err != nil {
return retry.ExpectedError(err)
}
if string(b) != contents {
return retry.ExpectedErrorf("contents don't match %q != %q", string(b), contents)
}
r, err := suite.state.Get(suite.ctx, resource.NewMetadata(files.NamespaceName, files.EtcFileStatusType, filename, resource.VersionUndefined))
if err != nil {
if state.IsNotFoundError(err) {
return retry.ExpectedError(err)
}
return err
}
version := r.(*files.EtcFileStatus).TypedSpec().SpecVersion
expected, err := strconv.Atoi(expectedVersion.String())
suite.Require().NoError(err)
ver, err := strconv.Atoi(version)
suite.Require().NoError(err)
if ver < expected {
return retry.ExpectedErrorf("version mismatch %s > %s", expectedVersion, version)
}
return nil
}
func (suite *EtcFileSuite) TestFiles() {
etcFileSpec := files.NewEtcFileSpec(files.NamespaceName, "test1")
etcFileSpec.TypedSpec().Contents = []byte("foo")
etcFileSpec.TypedSpec().Mode = 0o644
// create "read-only" mock (in Talos it's part of rootfs)
suite.T().Logf("mock created %q", filepath.Join(suite.etcPath, etcFileSpec.Metadata().ID()))
suite.Require().NoError(os.WriteFile(filepath.Join(suite.etcPath, etcFileSpec.Metadata().ID()), nil, 0o644))
suite.Require().NoError(suite.state.Create(suite.ctx, etcFileSpec))
suite.Assert().NoError(retry.Constant(5*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertEtcFile("test1", "foo", etcFileSpec.Metadata().Version())
}))
for _, r := range []resource.Resource{etcFileSpec} {
for {
ready, err := suite.state.Teardown(suite.ctx, r.Metadata())
suite.Require().NoError(err)
if ready {
break
}
time.Sleep(100 * time.Millisecond)
}
}
}
func TestEtcFileSuite(t *testing.T) {
suite.Run(t, new(EtcFileSuite))
}

View File

@ -0,0 +1,6 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package files provides controllers which manage file resources.
package files

View File

@ -0,0 +1,214 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package network
import (
"bytes"
"context"
"fmt"
"html/template"
"strings"
"github.com/AlekSi/pointer"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"go.uber.org/zap"
talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/files"
"github.com/talos-systems/talos/pkg/resources/network"
)
// EtcFileController creates /etc/hostname and /etc/resolv.conf files based on finalized network configuration.
type EtcFileController struct{}
// Name implements controller.Controller interface.
func (ctrl *EtcFileController) Name() string {
return "network.EtcFileController"
}
// Inputs implements controller.Controller interface.
func (ctrl *EtcFileController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: config.NamespaceName,
Type: config.MachineConfigType,
ID: pointer.ToString(config.V1Alpha1ID),
Kind: controller.InputWeak,
},
{
Namespace: network.NamespaceName,
Type: network.HostnameStatusType,
ID: pointer.ToString(network.HostnameID),
Kind: controller.InputWeak,
},
{
Namespace: network.NamespaceName,
Type: network.ResolverStatusType,
ID: pointer.ToString(network.ResolverID),
Kind: controller.InputWeak,
},
{
Namespace: network.NamespaceName,
Type: network.NodeAddressType,
ID: pointer.ToString(network.NodeAddressDefaultID),
Kind: controller.InputWeak,
},
}
}
// Outputs implements controller.Controller interface.
func (ctrl *EtcFileController) Outputs() []controller.Output {
return []controller.Output{
{
Type: files.EtcFileSpecType,
Kind: controller.OutputShared,
},
}
}
// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *EtcFileController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}
var cfgProvider talosconfig.Provider
cfg, err := r.Get(ctx, resource.NewMetadata(config.NamespaceName, config.MachineConfigType, config.V1Alpha1ID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting config: %w", err)
}
} else {
cfgProvider = cfg.(*config.MachineConfig).Config()
}
var resolverStatus *network.ResolverStatusSpec
rStatus, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.ResolverStatusType, network.ResolverID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting resolver status: %w", err)
}
} else {
resolverStatus = rStatus.(*network.ResolverStatus).TypedSpec()
}
var hostnameStatus *network.HostnameStatusSpec
hStatus, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.HostnameStatusType, network.HostnameID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting hostname status: %w", err)
}
} else {
hostnameStatus = hStatus.(*network.HostnameStatus).TypedSpec()
}
var nodeAddressStatus *network.NodeAddressSpec
naStatus, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.NodeAddressType, network.NodeAddressDefaultID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting network address status: %w", err)
}
} else {
nodeAddressStatus = naStatus.(*network.NodeAddress).TypedSpec()
}
if resolverStatus != nil {
if err = r.Modify(ctx, files.NewEtcFileSpec(files.NamespaceName, "resolv.conf"),
func(r resource.Resource) error {
r.(*files.EtcFileSpec).TypedSpec().Contents = ctrl.renderResolvConf(resolverStatus, hostnameStatus)
r.(*files.EtcFileSpec).TypedSpec().Mode = 0o644
return nil
}); err != nil {
return fmt.Errorf("error modifying resolv.conf: %w", err)
}
}
if hostnameStatus != nil && nodeAddressStatus != nil {
if err = r.Modify(ctx, files.NewEtcFileSpec(files.NamespaceName, "hosts"),
func(r resource.Resource) error {
r.(*files.EtcFileSpec).TypedSpec().Contents, err = ctrl.renderHosts(hostnameStatus, nodeAddressStatus, cfgProvider)
r.(*files.EtcFileSpec).TypedSpec().Mode = 0o644
return err
}); err != nil {
return fmt.Errorf("error modifying resolv.conf: %w", err)
}
}
}
}
func (ctrl *EtcFileController) renderResolvConf(resolverStatus *network.ResolverStatusSpec, hostnameStatus *network.HostnameStatusSpec) []byte {
var buf bytes.Buffer
for i, resolver := range resolverStatus.DNSServers {
if i >= 3 {
// only use firt 3 nameservers, see MAXNS in https://linux.die.net/man/5/resolv.conf
break
}
fmt.Fprintf(&buf, "nameserver %s\n", resolver)
}
if hostnameStatus != nil && hostnameStatus.Domainname != "" {
fmt.Fprintf(&buf, "\nsearch %s\n", hostnameStatus.Domainname)
}
return buf.Bytes()
}
var hostsTemplate = template.Must(template.New("hosts").Parse(strings.TrimSpace(`
127.0.0.1 localhost
{{ .IP }} {{ .Hostname }} {{ if ne .Hostname .Alias }}{{ .Alias }}{{ end }}
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
{{- with .ExtraHosts }}
{{ range . }}
{{ .IP }} {{ range .Aliases }}{{.}} {{ end -}}
{{ end -}}
{{ end -}}
`)))
func (ctrl *EtcFileController) renderHosts(hostnameStatus *network.HostnameStatusSpec, nodeAddressStatus *network.NodeAddressSpec, cfgProvider talosconfig.Provider) ([]byte, error) {
var buf bytes.Buffer
extraHosts := []talosconfig.ExtraHost{}
if cfgProvider != nil {
extraHosts = cfgProvider.Machine().Network().ExtraHosts()
}
data := struct {
IP string
Hostname string
Alias string
ExtraHosts []talosconfig.ExtraHost
}{
IP: nodeAddressStatus.Addresses[0].String(),
Hostname: hostnameStatus.FQDN(),
Alias: hostnameStatus.Hostname,
ExtraHosts: extraHosts,
}
if err := hostsTemplate.Execute(&buf, data); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,245 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//nolint:dupl
package network_test
import (
"context"
"fmt"
"log"
"net/url"
"sync"
"testing"
"time"
"github.com/cosi-project/runtime/pkg/controller/runtime"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/go-retry/retry"
"inet.af/netaddr"
netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network"
"github.com/talos-systems/talos/pkg/logging"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/files"
"github.com/talos-systems/talos/pkg/resources/network"
)
type EtcFileConfigSuite struct {
suite.Suite
state state.State
runtime *runtime.Runtime
wg sync.WaitGroup
ctx context.Context
ctxCancel context.CancelFunc
cfg *config.MachineConfig
defaultAddress *network.NodeAddress
hostnameStatus *network.HostnameStatus
resolverStatus *network.ResolverStatus
}
func (suite *EtcFileConfigSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)
suite.state = state.WrapCore(namespaced.NewState(inmem.Build))
var err error
suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer()))
suite.Require().NoError(err)
suite.startRuntime()
suite.Require().NoError(suite.runtime.RegisterController(&netctrl.EtcFileController{}))
u, err := url.Parse("https://foo:6443")
suite.Require().NoError(err)
suite.cfg = config.NewMachineConfig(&v1alpha1.Config{
ConfigVersion: "v1alpha1",
MachineConfig: &v1alpha1.MachineConfig{
MachineNetwork: &v1alpha1.NetworkConfig{
ExtraHostEntries: []*v1alpha1.ExtraHost{
{
HostIP: "10.0.0.1",
HostAliases: []string{"a", "b"},
},
{
HostIP: "10.0.0.2",
HostAliases: []string{"c", "d"},
},
},
},
},
ClusterConfig: &v1alpha1.ClusterConfig{
ControlPlane: &v1alpha1.ControlPlaneConfig{
Endpoint: &v1alpha1.Endpoint{
URL: u,
},
},
},
})
suite.defaultAddress = network.NewNodeAddress(network.NamespaceName, network.NodeAddressDefaultID)
suite.defaultAddress.TypedSpec().Addresses = []netaddr.IP{netaddr.MustParseIP("33.11.22.44")}
suite.hostnameStatus = network.NewHostnameStatus(network.NamespaceName, network.HostnameID)
suite.hostnameStatus.TypedSpec().Hostname = "foo"
suite.hostnameStatus.TypedSpec().Domainname = "example.com"
suite.resolverStatus = network.NewResolverStatus(network.NamespaceName, network.ResolverID)
suite.resolverStatus.TypedSpec().DNSServers = []netaddr.IP{netaddr.MustParseIP("1.1.1.1"), netaddr.MustParseIP("2.2.2.2"), netaddr.MustParseIP("3.3.3.3"), netaddr.MustParseIP("4.4.4.4")}
}
func (suite *EtcFileConfigSuite) startRuntime() {
suite.wg.Add(1)
go func() {
defer suite.wg.Done()
suite.Assert().NoError(suite.runtime.Run(suite.ctx))
}()
}
func (suite *EtcFileConfigSuite) assertEtcFiles(requiredIDs []string, check func(*files.EtcFileSpec) error) error {
missingIDs := make(map[string]struct{}, len(requiredIDs))
for _, id := range requiredIDs {
missingIDs[id] = struct{}{}
}
resources, err := suite.state.List(suite.ctx, resource.NewMetadata(files.NamespaceName, files.EtcFileSpecType, "", resource.VersionUndefined))
if err != nil {
return err
}
for _, res := range resources.Items {
_, required := missingIDs[res.Metadata().ID()]
if !required {
continue
}
delete(missingIDs, res.Metadata().ID())
if err = check(res.(*files.EtcFileSpec)); err != nil {
return retry.ExpectedError(err)
}
}
if len(missingIDs) > 0 {
return retry.ExpectedError(fmt.Errorf("some resources are missing: %q", missingIDs))
}
return nil
}
func (suite *EtcFileConfigSuite) assertNoEtcFile(id string) error {
resources, err := suite.state.List(suite.ctx, resource.NewMetadata(files.NamespaceName, files.EtcFileSpecType, "", resource.VersionUndefined))
if err != nil {
return err
}
for _, res := range resources.Items {
if res.Metadata().ID() == id {
return retry.ExpectedError(fmt.Errorf("spec %q is still there", id))
}
}
return nil
}
func (suite *EtcFileConfigSuite) testFiles(resources []resource.Resource, resolvConf, hosts string) {
for _, r := range resources {
suite.Require().NoError(suite.state.Create(suite.ctx, r))
}
expectedIds, unexpectedIds := []string{}, []string{}
if resolvConf != "" {
expectedIds = append(expectedIds, "resolv.conf")
} else {
unexpectedIds = append(unexpectedIds, "resolv.conf")
}
if hosts != "" {
expectedIds = append(expectedIds, "hosts")
} else {
unexpectedIds = append(unexpectedIds, "hosts")
}
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertEtcFiles(
expectedIds,
func(r *files.EtcFileSpec) error {
switch r.Metadata().ID() {
case "hosts":
suite.Assert().Equal(hosts, string(r.TypedSpec().Contents))
case "resolv.conf":
suite.Assert().Equal(resolvConf, string(r.TypedSpec().Contents))
}
return nil
})
}))
for _, id := range unexpectedIds {
id := id
suite.Assert().NoError(retry.Constant(1*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertNoEtcFile(id)
}))
}
}
func (suite *EtcFileConfigSuite) TestComplete() {
suite.testFiles([]resource.Resource{suite.cfg, suite.defaultAddress, suite.hostnameStatus, suite.resolverStatus},
"nameserver 1.1.1.1\nnameserver 2.2.2.2\nnameserver 3.3.3.3\n\nsearch example.com\n",
"127.0.0.1 localhost\n33.11.22.44 foo.example.com foo\n::1 localhost ip6-localhost ip6-loopback\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\n\n10.0.0.1 a b \n10.0.0.2 c d ", //nolint:lll
)
}
func (suite *EtcFileConfigSuite) TestNoExtraHosts() {
suite.testFiles([]resource.Resource{suite.defaultAddress, suite.hostnameStatus, suite.resolverStatus},
"nameserver 1.1.1.1\nnameserver 2.2.2.2\nnameserver 3.3.3.3\n\nsearch example.com\n",
"127.0.0.1 localhost\n33.11.22.44 foo.example.com foo\n::1 localhost ip6-localhost ip6-loopback\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters",
)
}
func (suite *EtcFileConfigSuite) TestNoDomainname() {
suite.hostnameStatus.TypedSpec().Domainname = ""
suite.testFiles([]resource.Resource{suite.defaultAddress, suite.hostnameStatus, suite.resolverStatus},
"nameserver 1.1.1.1\nnameserver 2.2.2.2\nnameserver 3.3.3.3\n",
"127.0.0.1 localhost\n33.11.22.44 foo \n::1 localhost ip6-localhost ip6-loopback\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters",
)
}
func (suite *EtcFileConfigSuite) TestOnlyResolvers() {
suite.testFiles([]resource.Resource{suite.resolverStatus},
"nameserver 1.1.1.1\nnameserver 2.2.2.2\nnameserver 3.3.3.3\n",
"",
)
}
func (suite *EtcFileConfigSuite) TestOnlyHostname() {
suite.testFiles([]resource.Resource{suite.defaultAddress, suite.hostnameStatus},
"",
"127.0.0.1 localhost\n33.11.22.44 foo.example.com foo\n::1 localhost ip6-localhost ip6-loopback\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters",
)
}
func TestEtcFileConfigSuite(t *testing.T) {
suite.Run(t, new(EtcFileConfigSuite))
}

View File

@ -13,6 +13,7 @@ import (
"go.uber.org/zap/zapcore"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/config"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/files"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/k8s"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network"
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/perf"
@ -21,6 +22,7 @@ import (
"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/v1alpha1"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
"github.com/talos-systems/talos/pkg/logging"
"github.com/talos-systems/talos/pkg/machinery/constants"
)
// Controller implements runtime.V1alpha2Controller.
@ -64,6 +66,10 @@ func (ctrl *Controller) Run(ctx context.Context) error {
},
&config.MachineTypeController{},
&config.K8sControlPlaneController{},
&files.EtcFileController{
EtcPath: "/etc",
ShadowPath: constants.SystemEtcPath,
},
&k8s.ControlPlaneStaticPodController{},
&k8s.ExtraManifestController{},
&k8s.KubeletStaticPodController{},
@ -76,6 +82,8 @@ func (ctrl *Controller) Run(ctx context.Context) error {
&network.AddressMergeController{},
&network.AddressSpecController{},
&network.AddressStatusController{},
// TODO: disabled to avoid conflict with networkd
// &network.EtcFileController{},
&network.HostnameConfigController{
Cmdline: procfs.ProcCmdline(),
},

View File

@ -15,6 +15,7 @@ import (
talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/resources/config"
"github.com/talos-systems/talos/pkg/resources/files"
"github.com/talos-systems/talos/pkg/resources/k8s"
"github.com/talos-systems/talos/pkg/resources/network"
"github.com/talos-systems/talos/pkg/resources/perf"
@ -56,10 +57,11 @@ func NewState() (*State, error) {
}{
{v1alpha1.NamespaceName, "Talos v1alpha1 subsystems glue resources."},
{config.NamespaceName, "Talos node configuration."},
{files.NamespaceName, "Files and file-like resources."},
{k8s.ControlPlaneNamespaceName, "Kubernetes control plane resources."},
{secrets.NamespaceName, "Resources with secret material."},
{network.NamespaceName, "Networking resources."},
{network.ConfigNamespaceName, "Networking configuration resources."},
{secrets.NamespaceName, "Resources with secret material."},
{perf.NamespaceName, "Stats resources."},
} {
if err := s.namespaceRegistry.Register(ctx, ns.name, ns.description); err != nil {
@ -74,6 +76,8 @@ func NewState() (*State, error) {
&config.MachineConfig{},
&config.MachineType{},
&config.K8sControlPlane{},
&files.EtcFileSpec{},
&files.EtcFileStatus{},
&k8s.Manifest{},
&k8s.ManifestStatus{},
&k8s.StaticPod{},

View File

@ -0,0 +1,80 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package files
import (
"fmt"
"io/fs"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
)
// EtcFileSpecType is type of EtcFile resource.
const EtcFileSpecType = resource.Type("EtcFileSpecs.files.talos.dev")
// EtcFileSpec resource holds contents of the file which should be put to `/etc` directory.
type EtcFileSpec struct {
md resource.Metadata
spec EtcFileSpecSpec
}
// EtcFileSpecSpec describes status of rendered secrets.
type EtcFileSpecSpec struct {
Contents []byte `yaml:"contents"`
Mode fs.FileMode `yaml:"mode"`
}
// NewEtcFileSpec initializes a EtcFileSpec resource.
func NewEtcFileSpec(namespace resource.Namespace, id resource.ID) *EtcFileSpec {
r := &EtcFileSpec{
md: resource.NewMetadata(namespace, EtcFileSpecType, id, resource.VersionUndefined),
spec: EtcFileSpecSpec{},
}
r.md.BumpVersion()
return r
}
// Metadata implements resource.Resource.
func (r *EtcFileSpec) Metadata() *resource.Metadata {
return &r.md
}
// Spec implements resource.Resource.
func (r *EtcFileSpec) Spec() interface{} {
return r.spec
}
func (r *EtcFileSpec) String() string {
return fmt.Sprintf("network.EtcFileSpec(%q)", r.md.ID())
}
// DeepCopy implements resource.Resource.
func (r *EtcFileSpec) DeepCopy() resource.Resource {
return &EtcFileSpec{
md: r.md,
spec: EtcFileSpecSpec{
Contents: append([]byte(nil), r.spec.Contents...),
Mode: r.spec.Mode,
},
}
}
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
func (r *EtcFileSpec) ResourceDefinition() meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: EtcFileSpecType,
Aliases: []resource.Type{},
DefaultNamespace: NamespaceName,
PrintColumns: []meta.PrintColumn{},
}
}
// TypedSpec allows to access the Spec with the proper type.
func (r *EtcFileSpec) TypedSpec() *EtcFileSpecSpec {
return &r.spec
}

View File

@ -0,0 +1,75 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package files
import (
"fmt"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/resource/meta"
)
// EtcFileStatusType is type of EtcFile resource.
const EtcFileStatusType = resource.Type("EtcFileStatuses.files.talos.dev")
// EtcFileStatus resource holds contents of the file which should be put to `/etc` directory.
type EtcFileStatus struct {
md resource.Metadata
spec EtcFileStatusSpec
}
// EtcFileStatusSpec describes status of rendered secrets.
type EtcFileStatusSpec struct {
SpecVersion string `yaml:"specVersion"`
}
// NewEtcFileStatus initializes a EtcFileStatus resource.
func NewEtcFileStatus(namespace resource.Namespace, id resource.ID) *EtcFileStatus {
r := &EtcFileStatus{
md: resource.NewMetadata(namespace, EtcFileStatusType, id, resource.VersionUndefined),
spec: EtcFileStatusSpec{},
}
r.md.BumpVersion()
return r
}
// Metadata implements resource.Resource.
func (r *EtcFileStatus) Metadata() *resource.Metadata {
return &r.md
}
// Spec implements resource.Resource.
func (r *EtcFileStatus) Spec() interface{} {
return r.spec
}
func (r *EtcFileStatus) String() string {
return fmt.Sprintf("network.EtcFileStatus(%q)", r.md.ID())
}
// DeepCopy implements resource.Resource.
func (r *EtcFileStatus) DeepCopy() resource.Resource {
return &EtcFileStatus{
md: r.md,
spec: r.spec,
}
}
// ResourceDefinition implements meta.ResourceDefinitionProvider interface.
func (r *EtcFileStatus) ResourceDefinition() meta.ResourceDefinitionSpec {
return meta.ResourceDefinitionSpec{
Type: EtcFileStatusType,
Aliases: []resource.Type{},
DefaultNamespace: NamespaceName,
PrintColumns: []meta.PrintColumn{},
}
}
// TypedSpec allows to access the Spec with the proper type.
func (r *EtcFileStatus) TypedSpec() *EtcFileStatusSpec {
return &r.spec
}

View File

@ -0,0 +1,11 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package files provides resources which describe files on disk.
package files
import "github.com/cosi-project/runtime/pkg/resource"
// NamespaceName contains file resources.
const NamespaceName resource.Namespace = "files"

View File

@ -0,0 +1,33 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package files_test
import (
"context"
"testing"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/cosi-project/runtime/pkg/state/registry"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/pkg/resources/files"
)
func TestRegisterResource(t *testing.T) {
ctx := context.TODO()
resources := state.WrapCore(namespaced.NewState(inmem.Build))
resourceRegistry := registry.NewResourceRegistry(resources)
for _, resource := range []resource.Resource{
&files.EtcFileSpec{},
&files.EtcFileStatus{},
} {
assert.NoError(t, resourceRegistry.Register(ctx, resource))
}
}