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:
Andrey Smirnov 2020-11-20 16:49:27 +03:00 committed by talos-bot
parent 2588e2960b
commit 9a32e34cb1
16 changed files with 1227 additions and 1093 deletions

View File

@ -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; }

View File

@ -23,6 +23,7 @@ var applyConfigCmdFlags struct {
filename string
insecure bool
interactive bool
noReboot bool
}
// applyConfigCmd represents the applyConfiguration command.
@ -108,7 +109,8 @@ var applyConfigCmd = &cobra.Command{
}
if _, err := c.ApplyConfiguration(ctx, &machineapi.ApplyConfigurationRequest{
Data: cfgBytes,
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)
}

View File

@ -13,6 +13,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
@ -93,19 +94,42 @@ 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 err = s.Controller.Runtime().SetConfig(in.GetData()); err != nil {
return nil, err
}
go func() {
if err = s.Controller.Run(runtime.SequenceApplyConfiguration, in); err != nil {
log.Println("apply configuration failed:", err)
if err != runtime.ErrLocked {
s.server.GracefulStop()
}
if !in.NoReboot {
if err = s.Controller.Runtime().SetConfig(in.GetData()); err != nil {
return nil, err
}
}()
go func() {
if err = s.Controller.Run(runtime.SequenceApplyConfiguration, in); err != nil {
log.Println("apply configuration failed:", err)
if err != runtime.ErrLocked {
s.server.GracefulStop()
}
}
}()
} 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{

View File

@ -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

View File

@ -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

View File

@ -58,11 +58,12 @@ func (*Sequencer) ApplyConfiguration(r runtime.Runtime, req *machineapi.ApplyCon
).Append(
"unmountState",
UnmountStatePartition,
).AppendList(stopAllPhaselist(r)).
Append(
"reboot",
Reboot,
)
).AppendList(
stopAllPhaselist(r),
).Append(
"reboot",
Reboot,
)
return phases
}

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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

View 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)
}

View File

@ -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)

View File

@ -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))

View File

@ -344,6 +344,7 @@ node.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| data | [bytes](#bytes) | | |
| no_reboot | [bool](#bool) | | |

View File

@ -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