mirror of
				https://github.com/siderolabs/talos.git
				synced 2025-11-04 10:21:13 +01:00 
			
		
		
		
	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:
		
							parent
							
								
									5aede1a833
								
							
						
					
					
						commit
						e74f789b01
					
				
							
								
								
									
										186
									
								
								internal/app/machined/pkg/controllers/files/etcfile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								internal/app/machined/pkg/controllers/files/etcfile.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										142
									
								
								internal/app/machined/pkg/controllers/files/etcfile_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								internal/app/machined/pkg/controllers/files/etcfile_test.go
									
									
									
									
									
										Normal 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))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										6
									
								
								internal/app/machined/pkg/controllers/files/files.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								internal/app/machined/pkg/controllers/files/files.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
							
								
								
									
										214
									
								
								internal/app/machined/pkg/controllers/network/etcfile.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								internal/app/machined/pkg/controllers/network/etcfile.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										245
									
								
								internal/app/machined/pkg/controllers/network/etcfile_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								internal/app/machined/pkg/controllers/network/etcfile_test.go
									
									
									
									
									
										Normal 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))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -13,6 +13,7 @@ import (
 | 
				
			|||||||
	"go.uber.org/zap/zapcore"
 | 
						"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/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/k8s"
 | 
				
			||||||
	"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network"
 | 
						"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network"
 | 
				
			||||||
	"github.com/talos-systems/talos/internal/app/machined/pkg/controllers/perf"
 | 
						"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/controllers/v1alpha1"
 | 
				
			||||||
	"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
 | 
						"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
 | 
				
			||||||
	"github.com/talos-systems/talos/pkg/logging"
 | 
						"github.com/talos-systems/talos/pkg/logging"
 | 
				
			||||||
 | 
						"github.com/talos-systems/talos/pkg/machinery/constants"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Controller implements runtime.V1alpha2Controller.
 | 
					// Controller implements runtime.V1alpha2Controller.
 | 
				
			||||||
@ -64,6 +66,10 @@ func (ctrl *Controller) Run(ctx context.Context) error {
 | 
				
			|||||||
		},
 | 
							},
 | 
				
			||||||
		&config.MachineTypeController{},
 | 
							&config.MachineTypeController{},
 | 
				
			||||||
		&config.K8sControlPlaneController{},
 | 
							&config.K8sControlPlaneController{},
 | 
				
			||||||
 | 
							&files.EtcFileController{
 | 
				
			||||||
 | 
								EtcPath:    "/etc",
 | 
				
			||||||
 | 
								ShadowPath: constants.SystemEtcPath,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		&k8s.ControlPlaneStaticPodController{},
 | 
							&k8s.ControlPlaneStaticPodController{},
 | 
				
			||||||
		&k8s.ExtraManifestController{},
 | 
							&k8s.ExtraManifestController{},
 | 
				
			||||||
		&k8s.KubeletStaticPodController{},
 | 
							&k8s.KubeletStaticPodController{},
 | 
				
			||||||
@ -76,6 +82,8 @@ func (ctrl *Controller) Run(ctx context.Context) error {
 | 
				
			|||||||
		&network.AddressMergeController{},
 | 
							&network.AddressMergeController{},
 | 
				
			||||||
		&network.AddressSpecController{},
 | 
							&network.AddressSpecController{},
 | 
				
			||||||
		&network.AddressStatusController{},
 | 
							&network.AddressStatusController{},
 | 
				
			||||||
 | 
							// TODO: disabled to avoid conflict with networkd
 | 
				
			||||||
 | 
							// &network.EtcFileController{},
 | 
				
			||||||
		&network.HostnameConfigController{
 | 
							&network.HostnameConfigController{
 | 
				
			||||||
			Cmdline: procfs.ProcCmdline(),
 | 
								Cmdline: procfs.ProcCmdline(),
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
				
			|||||||
@ -15,6 +15,7 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
 | 
						talosconfig "github.com/talos-systems/talos/pkg/machinery/config"
 | 
				
			||||||
	"github.com/talos-systems/talos/pkg/resources/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/k8s"
 | 
				
			||||||
	"github.com/talos-systems/talos/pkg/resources/network"
 | 
						"github.com/talos-systems/talos/pkg/resources/network"
 | 
				
			||||||
	"github.com/talos-systems/talos/pkg/resources/perf"
 | 
						"github.com/talos-systems/talos/pkg/resources/perf"
 | 
				
			||||||
@ -56,10 +57,11 @@ func NewState() (*State, error) {
 | 
				
			|||||||
	}{
 | 
						}{
 | 
				
			||||||
		{v1alpha1.NamespaceName, "Talos v1alpha1 subsystems glue resources."},
 | 
							{v1alpha1.NamespaceName, "Talos v1alpha1 subsystems glue resources."},
 | 
				
			||||||
		{config.NamespaceName, "Talos node configuration."},
 | 
							{config.NamespaceName, "Talos node configuration."},
 | 
				
			||||||
 | 
							{files.NamespaceName, "Files and file-like resources."},
 | 
				
			||||||
		{k8s.ControlPlaneNamespaceName, "Kubernetes control plane resources."},
 | 
							{k8s.ControlPlaneNamespaceName, "Kubernetes control plane resources."},
 | 
				
			||||||
		{secrets.NamespaceName, "Resources with secret material."},
 | 
					 | 
				
			||||||
		{network.NamespaceName, "Networking resources."},
 | 
							{network.NamespaceName, "Networking resources."},
 | 
				
			||||||
		{network.ConfigNamespaceName, "Networking configuration resources."},
 | 
							{network.ConfigNamespaceName, "Networking configuration resources."},
 | 
				
			||||||
 | 
							{secrets.NamespaceName, "Resources with secret material."},
 | 
				
			||||||
		{perf.NamespaceName, "Stats resources."},
 | 
							{perf.NamespaceName, "Stats resources."},
 | 
				
			||||||
	} {
 | 
						} {
 | 
				
			||||||
		if err := s.namespaceRegistry.Register(ctx, ns.name, ns.description); err != nil {
 | 
							if err := s.namespaceRegistry.Register(ctx, ns.name, ns.description); err != nil {
 | 
				
			||||||
@ -74,6 +76,8 @@ func NewState() (*State, error) {
 | 
				
			|||||||
		&config.MachineConfig{},
 | 
							&config.MachineConfig{},
 | 
				
			||||||
		&config.MachineType{},
 | 
							&config.MachineType{},
 | 
				
			||||||
		&config.K8sControlPlane{},
 | 
							&config.K8sControlPlane{},
 | 
				
			||||||
 | 
							&files.EtcFileSpec{},
 | 
				
			||||||
 | 
							&files.EtcFileStatus{},
 | 
				
			||||||
		&k8s.Manifest{},
 | 
							&k8s.Manifest{},
 | 
				
			||||||
		&k8s.ManifestStatus{},
 | 
							&k8s.ManifestStatus{},
 | 
				
			||||||
		&k8s.StaticPod{},
 | 
							&k8s.StaticPod{},
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										80
									
								
								pkg/resources/files/etcfile_spec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								pkg/resources/files/etcfile_spec.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										75
									
								
								pkg/resources/files/etcfile_status.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								pkg/resources/files/etcfile_status.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								pkg/resources/files/files.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								pkg/resources/files/files.go
									
									
									
									
									
										Normal 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"
 | 
				
			||||||
							
								
								
									
										33
									
								
								pkg/resources/files/files_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								pkg/resources/files/files_test.go
									
									
									
									
									
										Normal 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))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user