mirror of
https://github.com/siderolabs/talos.git
synced 2025-11-28 14:11:15 +01:00
feat: implement apply configuration without reboot
This allows config to be written to disk without being applied immediately. Small refactoring to extract common code paths. At first, I tried to implement this via the sequencer, but looks like it's too hard to get it right, as sequencer lacks context and config to be written is not applied to the runtime. Fixes #2828 Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
This commit is contained in:
parent
2588e2960b
commit
9a32e34cb1
@ -61,7 +61,10 @@ service MachineService {
|
||||
// rpc applyConfiguration
|
||||
// ApplyConfiguration describes a request to assert a new configuration upon a
|
||||
// node.
|
||||
message ApplyConfigurationRequest { bytes data = 1; }
|
||||
message ApplyConfigurationRequest {
|
||||
bytes data = 1;
|
||||
bool no_reboot = 2;
|
||||
}
|
||||
|
||||
// ApplyConfigurationResponse describes the response to a configuration request.
|
||||
message ApplyConfiguration { common.Metadata metadata = 1; }
|
||||
|
||||
@ -23,6 +23,7 @@ var applyConfigCmdFlags struct {
|
||||
filename string
|
||||
insecure bool
|
||||
interactive bool
|
||||
noReboot bool
|
||||
}
|
||||
|
||||
// applyConfigCmd represents the applyConfiguration command.
|
||||
@ -109,6 +110,7 @@ var applyConfigCmd = &cobra.Command{
|
||||
|
||||
if _, err := c.ApplyConfiguration(ctx, &machineapi.ApplyConfigurationRequest{
|
||||
Data: cfgBytes,
|
||||
NoReboot: applyConfigCmdFlags.noReboot,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error applying new configuration: %s", err)
|
||||
}
|
||||
@ -122,7 +124,8 @@ func init() {
|
||||
applyConfigCmd.Flags().StringVarP(&applyConfigCmdFlags.filename, "file", "f", "", "the filename of the updated configuration")
|
||||
applyConfigCmd.Flags().BoolVarP(&applyConfigCmdFlags.insecure, "insecure", "i", false, "apply the config using the insecure (encrypted with no auth) maintenance service")
|
||||
applyConfigCmd.Flags().StringSliceVar(&applyConfigCmdFlags.certFingerprints, "cert-fingerprint", nil, "list of server certificate fingeprints to accept (defaults to no check)")
|
||||
applyConfigCmd.Flags().BoolVarP(&applyConfigCmdFlags.interactive, "interactive", "", false, "apply the config using text based interactive mode")
|
||||
applyConfigCmd.Flags().BoolVar(&applyConfigCmdFlags.interactive, "interactive", false, "apply the config using text based interactive mode")
|
||||
applyConfigCmd.Flags().BoolVar(&applyConfigCmdFlags.noReboot, "no-reboot", false, "apply the config only after the reboot")
|
||||
|
||||
addCommand(applyConfigCmd)
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -93,6 +94,7 @@ func (s *Server) Register(obj *grpc.Server) {
|
||||
|
||||
// ApplyConfiguration implements machine.MachineService.
|
||||
func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfigurationRequest) (reply *machine.ApplyConfigurationResponse, err error) {
|
||||
if !in.NoReboot {
|
||||
if err = s.Controller.Runtime().SetConfig(in.GetData()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -106,6 +108,28 @@ func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfig
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
cfg, err := s.Controller.Runtime().ValidateConfig(in.GetData())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cfg.ApplyDynamicConfig(ctx, s.Controller.Runtime().State().Platform())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
b, err = cfg.Bytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = ioutil.WriteFile(constants.ConfigPath, b, 0o600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reply = &machine.ApplyConfigurationResponse{
|
||||
Messages: []*machine.ApplyConfiguration{
|
||||
|
||||
@ -4,11 +4,14 @@
|
||||
|
||||
package runtime
|
||||
|
||||
import "github.com/talos-systems/talos/pkg/machinery/config"
|
||||
import (
|
||||
"github.com/talos-systems/talos/pkg/machinery/config"
|
||||
)
|
||||
|
||||
// Runtime defines the runtime parameters.
|
||||
type Runtime interface {
|
||||
Config() config.Provider
|
||||
ValidateConfig([]byte) (config.Provider, error)
|
||||
SetConfig([]byte) error
|
||||
State() State
|
||||
Events() EventStream
|
||||
|
||||
@ -35,15 +35,25 @@ func (r *Runtime) Config() config.Provider {
|
||||
return r.c
|
||||
}
|
||||
|
||||
// SetConfig implements the Runtime interface.
|
||||
func (r *Runtime) SetConfig(b []byte) error {
|
||||
// ValidateConfig implements the Runtime interface.
|
||||
func (r *Runtime) ValidateConfig(b []byte) (config.Provider, error) {
|
||||
cfg, err := configloader.NewFromBytes(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse config: %w", err)
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
if err := cfg.Validate(r.State().Platform().Mode()); err != nil {
|
||||
return fmt.Errorf("failed to validate config: %w", err)
|
||||
return nil, fmt.Errorf("failed to validate config: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SetConfig implements the Runtime interface.
|
||||
func (r *Runtime) SetConfig(b []byte) error {
|
||||
cfg, err := r.ValidateConfig(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.c = cfg
|
||||
|
||||
@ -58,8 +58,9 @@ func (*Sequencer) ApplyConfiguration(r runtime.Runtime, req *machineapi.ApplyCon
|
||||
).Append(
|
||||
"unmountState",
|
||||
UnmountStatePartition,
|
||||
).AppendList(stopAllPhaselist(r)).
|
||||
Append(
|
||||
).AppendList(
|
||||
stopAllPhaselist(r),
|
||||
).Append(
|
||||
"reboot",
|
||||
Reboot,
|
||||
)
|
||||
|
||||
@ -485,31 +485,10 @@ func LoadConfig(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFu
|
||||
// SaveConfig represents the SaveConfig task.
|
||||
func SaveConfig(seq runtime.Sequence, data interface{}) (runtime.TaskExecutionFunc, string) {
|
||||
return func(ctx context.Context, logger *log.Logger, r runtime.Runtime) (err error) {
|
||||
saveCtx, ctxCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer ctxCancel()
|
||||
|
||||
hostname, err := r.State().Platform().Hostname(saveCtx)
|
||||
if err != nil {
|
||||
if err = r.Config().ApplyDynamicConfig(ctx, r.State().Platform()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hostname != nil {
|
||||
r.Config().Machine().Network().SetHostname(string(hostname))
|
||||
}
|
||||
|
||||
addrs, err := r.State().Platform().ExternalIPs(saveCtx)
|
||||
if err != nil {
|
||||
logger.Printf("certificates will be created without external IPs: %v", err)
|
||||
}
|
||||
|
||||
sans := make([]string, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
sans = append(sans, addr.String())
|
||||
}
|
||||
|
||||
r.Config().Machine().Security().SetCertSANs(sans)
|
||||
r.Config().Cluster().SetCertSANs(sans)
|
||||
|
||||
var b []byte
|
||||
|
||||
b, err = r.Config().Bytes()
|
||||
|
||||
@ -49,6 +49,10 @@ func (s *Server) Register(obj *grpc.Server) {
|
||||
|
||||
// ApplyConfiguration implements machine.MachineService.
|
||||
func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfigurationRequest) (reply *machine.ApplyConfigurationResponse, err error) {
|
||||
if in.NoReboot {
|
||||
return nil, fmt.Errorf("apply configuration without reboot is not supported in maintenance mode")
|
||||
}
|
||||
|
||||
cfgProvider, err := configloader.NewFromBytes(in.GetData())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
|
||||
@ -72,7 +72,7 @@ func (suite *NetworkdSuite) TestHostname() {
|
||||
suite.Assert().Equal(addr, net.ParseIP("192.168.0.10"))
|
||||
|
||||
// Static for computed hostname ( talos-ip )
|
||||
sampleConfig.Machine().Network().SetHostname("")
|
||||
sampleConfig.(*v1alpha1.Config).MachineConfig.MachineNetwork.NetworkHostname = ""
|
||||
|
||||
nwd, err = New(sampleConfig)
|
||||
suite.Require().NoError(err)
|
||||
@ -83,7 +83,7 @@ func (suite *NetworkdSuite) TestHostname() {
|
||||
suite.Assert().Equal(addr, net.ParseIP("192.168.0.10"))
|
||||
|
||||
// Static for hostname too long
|
||||
sampleConfig.Machine().Network().SetHostname("somereallyreallyreallylongstringthathasmorethan63charactersbecauseweneedtotestit")
|
||||
sampleConfig.(*v1alpha1.Config).MachineConfig.MachineNetwork.NetworkHostname = "somereallyreallyreallylongstringthathasmorethan63charactersbecauseweneedtotestit"
|
||||
|
||||
nwd, err = New(sampleConfig)
|
||||
suite.Require().NoError(err)
|
||||
@ -93,7 +93,7 @@ func (suite *NetworkdSuite) TestHostname() {
|
||||
suite.Require().Error(err)
|
||||
|
||||
// Static for hostname vs domain name
|
||||
sampleConfig.Machine().Network().SetHostname("dadjokes.biz.dev.com.org.io")
|
||||
sampleConfig.(*v1alpha1.Config).MachineConfig.MachineNetwork.NetworkHostname = "dadjokes.biz.dev.com.org.io"
|
||||
|
||||
nwd, err = New(sampleConfig)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
@ -40,6 +40,10 @@ const applyConfigTestSysctl = "net.ipv6.conf.all.accept_ra_mtu"
|
||||
|
||||
const applyConfigTestSysctlVal = "1"
|
||||
|
||||
const applyConfigNoRebootTestSysctl = "fs.file-max"
|
||||
|
||||
const applyConfigNoRebootTestSysctlVal = "500000"
|
||||
|
||||
const assertRebootedRebootTimeout = 10 * time.Minute
|
||||
|
||||
// ApplyConfigSuite ...
|
||||
@ -127,6 +131,52 @@ func (suite *ApplyConfigSuite) TestApply() {
|
||||
)
|
||||
}
|
||||
|
||||
// TestApplyNoReboot verifies the apply config API without reboot.
|
||||
func (suite *ApplyConfigSuite) TestApplyNoReboot() {
|
||||
suite.WaitForBootDone(suite.ctx)
|
||||
|
||||
node := suite.RandomDiscoveredNode()
|
||||
nodeCtx := client.WithNodes(suite.ctx, node)
|
||||
|
||||
provider, err := suite.readConfigFromNode(nodeCtx)
|
||||
suite.Require().NoError(err, "failed to read existing config from node %q", node)
|
||||
|
||||
provider.Machine().Sysctls()[applyConfigNoRebootTestSysctl] = applyConfigNoRebootTestSysctlVal
|
||||
|
||||
cfgDataOut, err := provider.Bytes()
|
||||
suite.Require().NoError(err, "failed to marshal updated machine config data (node %q)", node)
|
||||
|
||||
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
|
||||
NoReboot: true,
|
||||
Data: cfgDataOut,
|
||||
})
|
||||
suite.Require().NoError(err, "failed to apply deferred configuration (node %q): %w", node)
|
||||
|
||||
// Verify configuration change
|
||||
var newProvider config.Provider
|
||||
|
||||
newProvider, err = suite.readConfigFromNode(nodeCtx)
|
||||
|
||||
suite.Require().NoError(err, "failed to read updated configuration from node %q: %w", node)
|
||||
|
||||
suite.Assert().Equal(
|
||||
newProvider.Machine().Sysctls()[applyConfigNoRebootTestSysctl],
|
||||
applyConfigNoRebootTestSysctlVal,
|
||||
)
|
||||
|
||||
// revert back
|
||||
delete(provider.Machine().Sysctls(), applyConfigNoRebootTestSysctl)
|
||||
|
||||
cfgDataOut, err = provider.Bytes()
|
||||
suite.Require().NoError(err, "failed to marshal updated machine config data (node %q)", node)
|
||||
|
||||
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
|
||||
NoReboot: true,
|
||||
Data: cfgDataOut,
|
||||
})
|
||||
suite.Require().NoError(err, "failed to apply deferred configuration (node %q): %w", node)
|
||||
}
|
||||
|
||||
func (suite *ApplyConfigSuite) readConfigFromNode(nodeCtx context.Context) (config.Provider, error) {
|
||||
// Load the current node machine config
|
||||
cfgData := new(bytes.Buffer)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
16
pkg/machinery/config/dynamic.go
Normal file
16
pkg/machinery/config/dynamic.go
Normal file
@ -0,0 +1,16 @@
|
||||
// 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 config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
// DynamicConfigProvider provides additional configuration which is overlaid on top of existing configuration.
|
||||
type DynamicConfigProvider interface {
|
||||
Hostname(context.Context) ([]byte, error)
|
||||
ExternalIPs(context.Context) ([]net.IP, error)
|
||||
}
|
||||
@ -5,6 +5,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -24,6 +25,7 @@ type Provider interface {
|
||||
Machine() MachineConfig
|
||||
Cluster() ClusterConfig
|
||||
Validate(RuntimeMode) error
|
||||
ApplyDynamicConfig(context.Context, DynamicConfigProvider) error
|
||||
String() (string, error)
|
||||
Bytes() ([]byte, error)
|
||||
}
|
||||
@ -84,14 +86,12 @@ type Security interface {
|
||||
CA() *x509.PEMEncodedCertificateAndKey
|
||||
Token() string
|
||||
CertSANs() []string
|
||||
SetCertSANs([]string)
|
||||
}
|
||||
|
||||
// MachineNetwork defines the requirements for a config that pertains to network
|
||||
// related options.
|
||||
type MachineNetwork interface {
|
||||
Hostname() string
|
||||
SetHostname(string)
|
||||
Resolvers() []string
|
||||
Devices() []Device
|
||||
ExtraHosts() []ExtraHost
|
||||
@ -230,7 +230,6 @@ type ClusterConfig interface {
|
||||
Endpoint() *url.URL
|
||||
Token() Token
|
||||
CertSANs() []string
|
||||
SetCertSANs([]string)
|
||||
CA() *x509.PEMEncodedCertificateAndKey
|
||||
AESCBCEncryptionSecret() string
|
||||
Config(machine.Type) (string, error)
|
||||
|
||||
@ -5,9 +5,11 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
stdx509 "crypto/x509"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
goruntime "runtime"
|
||||
@ -70,6 +72,53 @@ func (c *Config) Bytes() (res []byte, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// ApplyDynamicConfig implements the config.Provider interface.
|
||||
func (c *Config) ApplyDynamicConfig(ctx context.Context, dynamicProvider config.DynamicConfigProvider) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if c.MachineConfig == nil {
|
||||
c.MachineConfig = &MachineConfig{}
|
||||
}
|
||||
|
||||
hostname, err := dynamicProvider.Hostname(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hostname != nil {
|
||||
if c.MachineConfig.MachineNetwork == nil {
|
||||
c.MachineConfig.MachineNetwork = &NetworkConfig{}
|
||||
}
|
||||
|
||||
c.MachineConfig.MachineNetwork.NetworkHostname = string(hostname)
|
||||
}
|
||||
|
||||
addrs, err := dynamicProvider.ExternalIPs(ctx)
|
||||
if err != nil {
|
||||
log.Printf("certificates will be created without external IPs: %v", err)
|
||||
}
|
||||
|
||||
sans := make([]string, 0, len(addrs))
|
||||
for _, addr := range addrs {
|
||||
sans = append(sans, addr.String())
|
||||
}
|
||||
|
||||
c.MachineConfig.MachineCertSANs = append(c.MachineConfig.MachineCertSANs, sans...)
|
||||
|
||||
if c.ClusterConfig == nil {
|
||||
c.ClusterConfig = &ClusterConfig{}
|
||||
}
|
||||
|
||||
if c.ClusterConfig.APIServerConfig == nil {
|
||||
c.ClusterConfig.APIServerConfig = &APIServerConfig{}
|
||||
}
|
||||
|
||||
c.ClusterConfig.APIServerConfig.CertSANs = append(c.ClusterConfig.APIServerConfig.CertSANs, sans...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Install implements the config.Provider interface.
|
||||
func (m *MachineConfig) Install() config.Install {
|
||||
if m.MachineInstall == nil {
|
||||
@ -179,11 +228,6 @@ func (m *MachineConfig) CertSANs() []string {
|
||||
return m.MachineCertSANs
|
||||
}
|
||||
|
||||
// SetCertSANs implements the config.Provider interface.
|
||||
func (m *MachineConfig) SetCertSANs(sans []string) {
|
||||
m.MachineCertSANs = append(m.MachineCertSANs, sans...)
|
||||
}
|
||||
|
||||
// Registries implements the config.Provider interface.
|
||||
func (m *MachineConfig) Registries() config.Registries {
|
||||
return &m.MachineRegistries
|
||||
@ -242,15 +286,6 @@ func (c *ClusterConfig) CertSANs() []string {
|
||||
return c.APIServerConfig.CertSANs
|
||||
}
|
||||
|
||||
// SetCertSANs implements the config.Provider interface.
|
||||
func (c *ClusterConfig) SetCertSANs(sans []string) {
|
||||
if c.APIServerConfig == nil {
|
||||
c.APIServerConfig = &APIServerConfig{}
|
||||
}
|
||||
|
||||
c.APIServerConfig.CertSANs = append(c.APIServerConfig.CertSANs, sans...)
|
||||
}
|
||||
|
||||
// CA implements the config.Provider interface.
|
||||
func (c *ClusterConfig) CA() *x509.PEMEncodedCertificateAndKey {
|
||||
return c.ClusterCA
|
||||
@ -641,11 +676,6 @@ func (n *NetworkConfig) Hostname() string {
|
||||
return n.NetworkHostname
|
||||
}
|
||||
|
||||
// SetHostname implements the config.Provider interface.
|
||||
func (n *NetworkConfig) SetHostname(hostname string) {
|
||||
n.NetworkHostname = hostname
|
||||
}
|
||||
|
||||
// Devices implements the config.Provider interface.
|
||||
func (n *NetworkConfig) Devices() []config.Device {
|
||||
interfaces := make([]config.Device, len(n.NetworkInterfaces))
|
||||
|
||||
@ -344,6 +344,7 @@ node.
|
||||
| Field | Type | Label | Description |
|
||||
| ----- | ---- | ----- | ----------- |
|
||||
| data | [bytes](#bytes) | | |
|
||||
| no_reboot | [bool](#bool) | | |
|
||||
|
||||
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ talosctl apply-config [flags]
|
||||
-h, --help help for apply-config
|
||||
-i, --insecure apply the config using the insecure (encrypted with no auth) maintenance service
|
||||
--interactive apply the config using text based interactive mode
|
||||
--no-reboot apply the config only after the reboot
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user