feat: replace flags with --mode in apply, edit and patch commands

Fixes: https://github.com/talos-systems/talos/issues/4588

Signed-off-by: Artem Chernyshev <artem.chernyshev@talos-systems.com>
This commit is contained in:
Artem Chernyshev 2022-01-10 19:09:20 +03:00
parent b09be2a69c
commit 2f2bdb26aa
No known key found for this signature in database
GPG Key ID: 9B9D0328B57B443F
30 changed files with 2173 additions and 1698 deletions

View File

@ -73,9 +73,24 @@ service MachineService {
// ApplyConfiguration describes a request to assert a new configuration upon a
// node.
message ApplyConfigurationRequest {
enum Mode {
REBOOT = 0;
AUTO = 1;
NO_REBOOT = 2;
STAGED = 3;
}
bytes data = 1;
bool on_reboot = 2;
bool immediate = 3;
// replaced by mode
bool on_reboot = 2 [
(common.remove_deprecated_field) = "v0.16",
deprecated = true
];
// replaced by mode
bool immediate = 3 [
(common.remove_deprecated_field) = "v0.16",
deprecated = true
];
Mode mode = 4;
}
// ApplyConfigurationResponse describes the response to a configuration request.
@ -83,6 +98,10 @@ message ApplyConfiguration {
common.Metadata metadata = 1;
// Configuration validation warnings.
repeated string warnings = 2;
// States which mode was actually chosen.
ApplyConfigurationRequest.Mode mode = 3;
// Human-readable message explaining the result of the apply configuration call.
string mode_details = 4;
}
message ApplyConfigurationResponse {

View File

@ -12,19 +12,17 @@ import (
"github.com/spf13/cobra"
"github.com/talos-systems/talos/cmd/talosctl/pkg/talos/helpers"
"github.com/talos-systems/talos/internal/pkg/tui/installer"
"github.com/talos-systems/talos/pkg/cli"
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/client"
)
var applyConfigCmdFlags struct {
helpers.Mode
certFingerprints []string
filename string
insecure bool
interactive bool
onReboot bool
immediate bool
}
// applyConfigCmd represents the applyConfiguration command.
@ -61,7 +59,7 @@ var applyConfigCmd = &cobra.Command{
if len(cfgBytes) < 1 {
return fmt.Errorf("no configuration data read")
}
} else if !applyConfigCmdFlags.interactive {
} else if !applyConfigCmdFlags.Interactive {
return fmt.Errorf("no filename supplied for configuration")
}
@ -74,7 +72,7 @@ var applyConfigCmd = &cobra.Command{
}
return withClient(func(ctx context.Context, c *client.Client) error {
if applyConfigCmdFlags.interactive {
if applyConfigCmdFlags.Interactive {
install := installer.NewInstaller()
node := Nodes[0]
@ -111,18 +109,16 @@ var applyConfigCmd = &cobra.Command{
resp, err := c.ApplyConfiguration(ctx, &machineapi.ApplyConfigurationRequest{
Data: cfgBytes,
OnReboot: applyConfigCmdFlags.onReboot,
Immediate: applyConfigCmdFlags.immediate,
Mode: applyConfigCmdFlags.Mode.Mode,
OnReboot: applyConfigCmdFlags.OnReboot,
Immediate: applyConfigCmdFlags.Immediate,
})
for _, m := range resp.GetMessages() {
for _, w := range m.GetWarnings() {
cli.Warning("%s", w)
}
}
if err != nil {
return fmt.Errorf("error applying new configuration: %s", err)
}
helpers.PrintApplyResults(resp)
return nil
})
},
@ -132,8 +128,6 @@ 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().BoolVar(&applyConfigCmdFlags.interactive, "interactive", false, "apply the config using text based interactive mode")
applyConfigCmd.Flags().BoolVar(&applyConfigCmdFlags.onReboot, "on-reboot", false, "apply the config on reboot")
applyConfigCmd.Flags().BoolVar(&applyConfigCmdFlags.immediate, "immediate", false, "apply the config immediately (without a reboot)")
helpers.AddModeFlags(&applyConfigCmdFlags.Mode, applyConfigCmd)
addCommand(applyConfigCmd)
}

View File

@ -19,16 +19,14 @@ import (
"k8s.io/kubectl/pkg/cmd/util/editor/crlf"
"github.com/talos-systems/talos/cmd/talosctl/pkg/talos/helpers"
"github.com/talos-systems/talos/pkg/cli"
"github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/client"
"github.com/talos-systems/talos/pkg/machinery/resources/config"
)
var editCmdFlags struct {
helpers.Mode
namespace string
immediate bool
onReboot bool
}
//nolint:gocyclo
@ -117,8 +115,9 @@ func editFn(c *client.Client) func(context.Context, client.ResourceResponse) err
resp, err := c.ApplyConfiguration(ctx, &machine.ApplyConfigurationRequest{
Data: edited,
Immediate: editCmdFlags.immediate,
OnReboot: editCmdFlags.onReboot,
Mode: editCmdFlags.Mode.Mode,
OnReboot: editCmdFlags.OnReboot,
Immediate: editCmdFlags.Immediate,
})
if err != nil {
lastError = err.Error()
@ -126,11 +125,7 @@ func editFn(c *client.Client) func(context.Context, client.ResourceResponse) err
continue
}
for _, m := range resp.GetMessages() {
for _, w := range m.GetWarnings() {
cli.Warning("%s", w)
}
}
helpers.PrintApplyResults(resp)
break
}
@ -181,7 +176,6 @@ or 'notepad' for Windows.`,
func init() {
editCmd.Flags().StringVar(&editCmdFlags.namespace, "namespace", "", "resource namespace (default is to use default namespace per resource)")
editCmd.Flags().BoolVar(&editCmdFlags.immediate, "immediate", false, "apply the change immediately (without a reboot)")
editCmd.Flags().BoolVar(&editCmdFlags.onReboot, "on-reboot", false, "apply the change on next reboot")
helpers.AddModeFlags(&editCmdFlags.Mode, editCmd)
addCommand(editCmd)
}

View File

@ -18,7 +18,6 @@ import (
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"github.com/talos-systems/talos/cmd/talosctl/pkg/talos/helpers"
"github.com/talos-systems/talos/pkg/cli"
"github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/client"
"github.com/talos-systems/talos/pkg/machinery/config/configpatcher"
@ -26,11 +25,10 @@ import (
)
var patchCmdFlags struct {
helpers.Mode
namespace string
patch string
patchFile string
immediate bool
onReboot bool
}
func patchFn(c *client.Client, patch jsonpatch.Patch) func(context.Context, client.ResourceResponse) error {
@ -55,8 +53,9 @@ func patchFn(c *client.Client, patch jsonpatch.Patch) func(context.Context, clie
resp, err := c.ApplyConfiguration(ctx, &machine.ApplyConfigurationRequest{
Data: patched,
Immediate: patchCmdFlags.immediate,
OnReboot: patchCmdFlags.onReboot,
Mode: patchCmdFlags.Mode.Mode,
OnReboot: patchCmdFlags.OnReboot,
Immediate: patchCmdFlags.Immediate,
})
if bytes.Equal(
@ -74,11 +73,7 @@ func patchFn(c *client.Client, patch jsonpatch.Patch) func(context.Context, clie
msg.Metadata.GetHostname(),
)
for _, m := range resp.GetMessages() {
for _, w := range m.GetWarnings() {
cli.Warning("%s", w)
}
}
helpers.PrintApplyResults(resp)
return err
}
@ -134,7 +129,6 @@ func init() {
patchCmd.Flags().StringVar(&patchCmdFlags.namespace, "namespace", "", "resource namespace (default is to use default namespace per resource)")
patchCmd.Flags().StringVar(&patchCmdFlags.patchFile, "patch-file", "", "a file containing a patch to be applied to the resource.")
patchCmd.Flags().StringVarP(&patchCmdFlags.patch, "patch", "p", "", "the patch to be applied to the resource file.")
patchCmd.Flags().BoolVar(&patchCmdFlags.immediate, "immediate", false, "apply the change immediately (without a reboot)")
patchCmd.Flags().BoolVar(&patchCmdFlags.onReboot, "on-reboot", false, "apply the change on next reboot")
helpers.AddModeFlags(&patchCmdFlags.Mode, patchCmd)
addCommand(patchCmd)
}

View File

@ -0,0 +1,131 @@
// 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 helpers
import (
"fmt"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/talos-systems/talos/pkg/cli"
"github.com/talos-systems/talos/pkg/machinery/api/machine"
)
// InteractiveMode fake mode value for the interactive config mode.
// Should be never passed to the API.
const InteractiveMode machine.ApplyConfigurationRequest_Mode = -1
// Mode apply, patch, edit config config update mode.
type Mode struct {
options map[string]machine.ApplyConfigurationRequest_Mode
Mode machine.ApplyConfigurationRequest_Mode
Immediate bool
Interactive bool
OnReboot bool
}
func (m Mode) String() string {
switch m.Mode {
case machine.ApplyConfigurationRequest_AUTO:
return modeAuto
case machine.ApplyConfigurationRequest_NO_REBOOT:
return modeNoReboot
case machine.ApplyConfigurationRequest_REBOOT:
return modeReboot
case machine.ApplyConfigurationRequest_STAGED:
return modeStaged
case InteractiveMode:
return modeInteractive
default:
return modeAuto
}
}
// Set implements Flag interface.
func (m *Mode) Set(value string) error {
mode, ok := m.options[value]
if !ok {
return fmt.Errorf("possible options are: %s", m.Type())
}
m.Mode = mode
//nolint:exhaustive
switch m.Mode {
case machine.ApplyConfigurationRequest_STAGED:
m.OnReboot = true
case machine.ApplyConfigurationRequest_NO_REBOOT:
m.Immediate = true
case InteractiveMode:
m.Interactive = true
}
return nil
}
// Type implements Flag interface.
func (m *Mode) Type() string {
options := make([]string, 0, len(m.options))
for s := range m.options {
options = append(options, s)
}
sort.Strings(options)
return strings.Join(options, ", ")
}
const (
modeAuto = "auto"
modeNoReboot = "no-reboot"
modeReboot = "reboot"
modeStaged = "staged"
modeInteractive = "interactive"
)
// AddModeFlags adds deprecated flags to the command and registers mode flag with it's parser.
func AddModeFlags(mode *Mode, command *cobra.Command) {
modes := map[string]machine.ApplyConfigurationRequest_Mode{
modeAuto: machine.ApplyConfigurationRequest_AUTO,
modeNoReboot: machine.ApplyConfigurationRequest_NO_REBOOT,
modeReboot: machine.ApplyConfigurationRequest_REBOOT,
modeStaged: machine.ApplyConfigurationRequest_STAGED,
}
deprecatedFlag := func(dest *bool, flag, usage, deprecationWarning string) {
command.Flags().BoolVar(dest, flag, false, fmt.Sprintf("%s (deprecated, replaced with --mode)", usage))
command.Flags().MarkDeprecated(flag, deprecationWarning) //nolint:errcheck
}
// TODO: remove in v0.16
deprecatedFlag(&mode.OnReboot, "on-reboot", "apply the config on reboot", "Use --mode=staged instead")
deprecatedFlag(&mode.Immediate, "immediate", "apply the config immediately (without a reboot)", "Use --mode=no-reboot instead")
if command.Use == "apply-config" {
deprecatedFlag(&mode.Interactive, "interactive", "apply the config using text based interactive mode", "Use --mode=interactive instead")
modes[modeInteractive] = InteractiveMode
}
mode.Mode = machine.ApplyConfigurationRequest_AUTO
mode.options = modes
command.Flags().VarP(mode, "mode", "m", "apply config mode")
}
// PrintApplyResults prints out all warnings and auto apply results.
func PrintApplyResults(resp *machine.ApplyConfigurationResponse) {
for _, m := range resp.GetMessages() {
for _, w := range m.GetWarnings() {
cli.Warning("%s", w)
}
if m.ModeDetails != "" {
fmt.Println(m.ModeDetails)
}
}
}

View File

@ -15,6 +15,19 @@ preface = """\
[notes]
[notes.applyconfig]
title = "Apply Config Enhancements"
description="""\
`talosctl apply/patch/edit` cli commands got revamped.
Separate flags `--on-reboot`, `--immediate`, `--interactive` were replaced
with a single `--mode` flag that can take the following values:
- `auto` new mode that automatically applies the configuration in immediate/reboot mode.
- `no-reboot` force apply immediately, if not possible, then fail.
- `reboot` force reboot with apply config.
- `staged` write new machine configuration to STATE, but don't apply it (it will be applied after a reboot).
- `interactive` starts interactive installer, only for `apply`.
"""
[notes.updates]
title = "Component Updates"
description="""\

View File

@ -137,20 +137,44 @@ func (s *Server) Register(obj *grpc.Server) {
//
//nolint:gocyclo
func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfigurationRequest) (*machine.ApplyConfigurationResponse, error) {
log.Printf("apply config request: immediate %v, on reboot %v", in.Immediate, in.OnReboot)
mode := in.Mode.String()
modeDetails := ""
// TODO: remove in v0.16
switch {
case in.Immediate: //nolint:staticcheck
in.Mode = machine.ApplyConfigurationRequest_NO_REBOOT
case in.OnReboot: //nolint:staticcheck
in.Mode = machine.ApplyConfigurationRequest_STAGED
}
cfgProvider, err := s.Controller.Runtime().LoadAndValidateConfig(in.GetData())
if err != nil {
return nil, err
}
// --immediate
if in.Immediate {
//nolint:exhaustive
switch in.Mode {
// --mode=no-reboot
case machine.ApplyConfigurationRequest_NO_REBOOT:
if err = s.Controller.Runtime().CanApplyImmediate(cfgProvider); err != nil {
return nil, err
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
// --mode=auto detect actual update mode
case machine.ApplyConfigurationRequest_AUTO:
if err = s.Controller.Runtime().CanApplyImmediate(cfgProvider); err != nil {
in.Mode = machine.ApplyConfigurationRequest_REBOOT
modeDetails = fmt.Sprintf("applied configuration with a reboot: %s", err)
} else {
in.Mode = machine.ApplyConfigurationRequest_NO_REBOOT
modeDetails = "applied configuration without a reboot"
}
mode = fmt.Sprintf("%s(%s)", mode, in.Mode)
}
log.Printf("apply config request: mode %s", strings.ToLower(mode))
cfg, err := cfgProvider.Bytes()
if err != nil {
return nil, err
@ -160,14 +184,17 @@ func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfig
return nil, err
}
switch {
// --immediate
case in.Immediate:
//nolint:exhaustive
switch in.Mode {
// --mode=no-reboot
case machine.ApplyConfigurationRequest_NO_REBOOT:
if err := s.Controller.Runtime().SetConfig(cfgProvider); err != nil {
return nil, err
}
// default, no `--on-reboot`
case !in.OnReboot:
// --mode=staged
case machine.ApplyConfigurationRequest_STAGED:
// --mode=reboot
case machine.ApplyConfigurationRequest_REBOOT:
go func() {
if err := s.Controller.Run(context.Background(), runtime.SequenceReboot, nil, runtime.WithTakeover()); err != nil {
if !runtime.IsRebootError(err) {
@ -179,11 +206,16 @@ func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfig
}
}
}()
default:
return nil, fmt.Errorf("incorrect mode '%s' specified for the apply config call", in.Mode.String())
}
return &machine.ApplyConfigurationResponse{
Messages: []*machine.ApplyConfiguration{
{},
{
Mode: in.Mode,
ModeDetails: modeDetails,
},
},
}, nil
}

View File

@ -120,7 +120,7 @@ func Run(ctx context.Context, logger *log.Logger, r runtime.Runtime) ([]byte, er
logger.Println("upload configuration using talosctl:")
logger.Printf("\ttalosctl apply-config --insecure --nodes %s --file <config.yaml>", firstIP)
logger.Println("or apply configuration using talosctl interactive installer:")
logger.Printf("\ttalosctl apply-config --insecure --nodes %s --interactive", firstIP)
logger.Printf("\ttalosctl apply-config --insecure --nodes %s --mode=interactive", firstIP)
logger.Println("optionally with node fingerprint check:")
logger.Printf("\ttalosctl apply-config --insecure --nodes %s --cert-fingerprint '%s' --file <config.yaml>", firstIP, certFingerprint)

View File

@ -8,6 +8,7 @@ import (
"context"
"fmt"
"log"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@ -54,8 +55,14 @@ func (s *Server) Register(obj *grpc.Server) {
// ApplyConfiguration implements machine.MachineService.
func (s *Server) ApplyConfiguration(ctx context.Context, in *machine.ApplyConfigurationRequest) (*machine.ApplyConfigurationResponse, error) {
if in.OnReboot {
return nil, fmt.Errorf("apply configuration on reboot is not supported in maintenance mode")
//nolint:exhaustive
switch in.Mode {
case machine.ApplyConfigurationRequest_REBOOT:
fallthrough
case machine.ApplyConfigurationRequest_AUTO:
default:
return nil, fmt.Errorf("apply configuration --mode='%s' is not supported in maintenance mode",
strings.ReplaceAll(strings.ToLower(in.Mode.String()), "_", "-"))
}
cfgProvider, err := configloader.NewFromBytes(in.GetData())

View File

@ -9,11 +9,15 @@ package api
import (
"context"
"errors"
"sort"
"testing"
"time"
"github.com/hashicorp/go-multierror"
"github.com/talos-systems/go-retry/retry"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/talos-systems/talos/internal/integration/base"
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
@ -108,6 +112,7 @@ func (suite *ApplyConfigSuite) TestApply() {
suite.AssertRebooted(suite.ctx, node, func(nodeCtx context.Context) error {
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
Data: cfgDataOut,
Mode: machineapi.ApplyConfigurationRequest_REBOOT,
})
if err != nil {
// It is expected that the connection will EOF here, so just log the error
@ -135,62 +140,67 @@ func (suite *ApplyConfigSuite) TestApply() {
)
}
// TestApplyOnReboot verifies the apply config API without reboot.
func (suite *ApplyConfigSuite) TestApplyOnReboot() {
suite.WaitForBootDone(suite.ctx)
// TestApplyWithoutReboot verifies the apply config API without reboot.
func (suite *ApplyConfigSuite) TestApplyWithoutReboot() {
for _, mode := range []machineapi.ApplyConfigurationRequest_Mode{
machineapi.ApplyConfigurationRequest_AUTO,
machineapi.ApplyConfigurationRequest_STAGED,
} {
suite.WaitForBootDone(suite.ctx)
node := suite.RandomDiscoveredNode()
suite.ClearConnectionRefused(suite.ctx, node)
node := suite.RandomDiscoveredNode()
suite.ClearConnectionRefused(suite.ctx, node)
nodeCtx := client.WithNodes(suite.ctx, node)
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, err := suite.ReadConfigFromNode(nodeCtx)
suite.Require().NoError(err, "failed to read existing config from node %q", node)
cfg, ok := provider.Raw().(*v1alpha1.Config)
suite.Require().True(ok)
cfg, ok := provider.Raw().(*v1alpha1.Config)
suite.Require().True(ok)
if cfg.MachineConfig.MachineSysctls == nil {
cfg.MachineConfig.MachineSysctls = make(map[string]string)
if cfg.MachineConfig.MachineSysctls == nil {
cfg.MachineConfig.MachineSysctls = make(map[string]string)
}
cfg.MachineConfig.MachineSysctls[applyConfigNoRebootTestSysctl] = applyConfigNoRebootTestSysctlVal
cfgDataOut, err := cfg.Bytes()
suite.Require().NoError(err, "failed to marshal updated machine config data (node %q)", node)
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
Data: cfgDataOut,
Mode: mode,
})
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,
)
cfg, ok = newProvider.Raw().(*v1alpha1.Config)
suite.Require().True(ok)
// revert back
delete(cfg.MachineConfig.MachineSysctls, applyConfigNoRebootTestSysctl)
cfgDataOut, err = cfg.Bytes()
suite.Require().NoError(err, "failed to marshal updated machine config data (node %q)", node)
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
Data: cfgDataOut,
Mode: mode,
})
suite.Require().NoError(err, "failed to apply deferred configuration (node %q): %w", node)
}
cfg.MachineConfig.MachineSysctls[applyConfigNoRebootTestSysctl] = applyConfigNoRebootTestSysctlVal
cfgDataOut, err := cfg.Bytes()
suite.Require().NoError(err, "failed to marshal updated machine config data (node %q)", node)
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
OnReboot: 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,
)
cfg, ok = newProvider.Raw().(*v1alpha1.Config)
suite.Require().True(ok)
// revert back
delete(cfg.MachineConfig.MachineSysctls, applyConfigNoRebootTestSysctl)
cfgDataOut, err = cfg.Bytes()
suite.Require().NoError(err, "failed to marshal updated machine config data (node %q)", node)
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
OnReboot: true,
Data: cfgDataOut,
})
suite.Require().NoError(err, "failed to apply deferred configuration (node %q): %w", node)
}
// TestApplyConfigRotateEncryptionSecrets verify key rotation by sequential apply config calls.
@ -288,6 +298,7 @@ func (suite *ApplyConfigSuite) TestApplyConfigRotateEncryptionSecrets() {
suite.AssertRebooted(suite.ctx, node, func(nodeCtx context.Context) error {
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
Data: data,
Mode: machineapi.ApplyConfigurationRequest_REBOOT,
})
if err != nil {
// It is expected that the connection will EOF here, so just log the error
@ -334,6 +345,47 @@ func (suite *ApplyConfigSuite) TestApplyConfigRotateEncryptionSecrets() {
}
}
// TestApplyNoReboot verifies the apply config API fails if NoReboot mode is requested on a field that can not be applied immediately.
func (suite *ApplyConfigSuite) TestApplyNoReboot() {
nodes := suite.DiscoverNodes(suite.ctx).NodesByType(machine.TypeWorker)
suite.Require().NotEmpty(nodes)
suite.WaitForBootDone(suite.ctx)
sort.Strings(nodes)
node := nodes[0]
nodeCtx := client.WithNodes(suite.ctx, node)
provider, err := suite.ReadConfigFromNode(nodeCtx)
suite.Assert().Nilf(err, "failed to read existing config from node %q: %w", node, err)
cfg, ok := provider.Raw().(*v1alpha1.Config)
suite.Require().True(ok)
// this won't be possible without a reboot
cfg.MachineConfig.MachineType = "controlplane"
cfgDataOut, err := cfg.Bytes()
suite.Assert().Nilf(err, "failed to marshal updated machine config data (node %q): %w", node, err)
_, err = suite.Client.ApplyConfiguration(nodeCtx, &machineapi.ApplyConfigurationRequest{
Data: cfgDataOut,
Mode: machineapi.ApplyConfigurationRequest_NO_REBOOT,
})
suite.Require().Error(err)
var (
errs *multierror.Error
nodeError *client.NodeError
)
suite.Require().True(errors.As(err, &errs))
suite.Require().True(errors.As(errs.Errors[0], &nodeError))
suite.Require().Equal(codes.InvalidArgument, status.Code(nodeError.Err))
}
func init() {
allSuites = append(allSuites, new(ApplyConfigSuite))
}

View File

@ -43,7 +43,7 @@ func (suite *PatchSuite) TestSuccess() {
data, err := json.Marshal(patch)
suite.Require().NoError(err)
suite.RunCLI([]string{"patch", "--nodes", node, "--patch", string(data), "machineconfig", "--immediate"})
suite.RunCLI([]string{"patch", "--nodes", node, "--patch", string(data), "machineconfig", "--mode=no-reboot"})
}
// TestError runs comand with error.

View File

@ -69,7 +69,8 @@ func patchNodeConfig(ctx context.Context, cluster UpgradeProvider, node string,
_, err = c.ApplyConfiguration(ctx, &machine.ApplyConfigurationRequest{
Data: cfgBytes,
Immediate: true,
Mode: machine.ApplyConfigurationRequest_NO_REBOOT,
Immediate: true, // keeping that for backward compatibility
})
if err != nil {
return fmt.Errorf("error applying config: %w", err)

File diff suppressed because it is too large Load Diff

View File

@ -57,6 +57,11 @@ func (m *ApplyConfigurationRequest) MarshalToSizedBufferVT(dAtA []byte) (int, er
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if m.Mode != 0 {
i = encodeVarint(dAtA, i, uint64(m.Mode))
i--
dAtA[i] = 0x20
}
if m.Immediate {
i--
if m.Immediate {
@ -117,6 +122,18 @@ func (m *ApplyConfiguration) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
i -= len(m.unknownFields)
copy(dAtA[i:], m.unknownFields)
}
if len(m.ModeDetails) > 0 {
i -= len(m.ModeDetails)
copy(dAtA[i:], m.ModeDetails)
i = encodeVarint(dAtA, i, uint64(len(m.ModeDetails)))
i--
dAtA[i] = 0x22
}
if m.Mode != 0 {
i = encodeVarint(dAtA, i, uint64(m.Mode))
i--
dAtA[i] = 0x18
}
if len(m.Warnings) > 0 {
for iNdEx := len(m.Warnings) - 1; iNdEx >= 0; iNdEx-- {
i -= len(m.Warnings[iNdEx])
@ -7889,6 +7906,9 @@ func (m *ApplyConfigurationRequest) SizeVT() (n int) {
if m.Immediate {
n += 2
}
if m.Mode != 0 {
n += 1 + sov(uint64(m.Mode))
}
if m.unknownFields != nil {
n += len(m.unknownFields)
}
@ -7917,6 +7937,13 @@ func (m *ApplyConfiguration) SizeVT() (n int) {
n += 1 + l + sov(uint64(l))
}
}
if m.Mode != 0 {
n += 1 + sov(uint64(m.Mode))
}
l = len(m.ModeDetails)
if l > 0 {
n += 1 + l + sov(uint64(l))
}
if m.unknownFields != nil {
n += len(m.unknownFields)
}
@ -11303,6 +11330,25 @@ func (m *ApplyConfigurationRequest) UnmarshalVT(dAtA []byte) error {
}
}
m.Immediate = bool(v != 0)
case 4:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Mode", wireType)
}
m.Mode = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Mode |= ApplyConfigurationRequest_Mode(b&0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skip(dAtA[iNdEx:])
@ -11431,6 +11477,57 @@ func (m *ApplyConfiguration) UnmarshalVT(dAtA []byte) error {
}
m.Warnings = append(m.Warnings, string(dAtA[iNdEx:postIndex]))
iNdEx = postIndex
case 3:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Mode", wireType)
}
m.Mode = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Mode |= ApplyConfigurationRequest_Mode(b&0x7F) << shift
if b < 0x80 {
break
}
}
case 4:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field ModeDetails", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflow
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= uint64(b&0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLength
}
postIndex := iNdEx + intStringLen
if postIndex < 0 {
return ErrInvalidLength
}
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.ModeDetails = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skip(dAtA[iNdEx:])

View File

@ -80,7 +80,7 @@ NODE NAMESPACE TYPE ID VERSION TYPE
```
Nodes with `init` type are incompatible with `etcd` recovery procedure.
`init` node can be converted to `controlplane` type with `talosctl edit mc --on-reboot` command followed
`init` node can be converted to `controlplane` type with `talosctl edit mc --mode=staged` command followed
by node reboot with `talosctl reboot` command.
### Preparing Control Plane Nodes

View File

@ -135,7 +135,7 @@ There is no in-place encryption support for the partitions right now, so to avoi
As such, migration from unencrypted to encrypted needs some additional handling, especially around explicitly wiping partitions.
- `apply-config` should be called with `--on-reboot` flag.
- `apply-config` should be called with `--mode=staged`.
- Partition should be wiped after `apply-config`, but before the reboot.
Edit your machine config and add the encryption configuration:
@ -144,10 +144,10 @@ Edit your machine config and add the encryption configuration:
vim config.yaml
```
Apply the configuration with `--on-reboot` flag:
Apply the configuration with `--mode=staged`:
```bash
talosctl apply-config -f config.yaml -n <node ip> --on-reboot
talosctl apply-config -f config.yaml -n <node ip> --mode=staged
```
Wipe the partition you're going to encrypt:

View File

@ -15,14 +15,29 @@ There are three `talosctl` commands which facilitate machine configuration updat
* `talosctl edit machineconfig` to launch an editor with existing node configuration, make changes and apply configuration back
* `talosctl patch machineconfig` to apply automated machine configuration via JSON patch
Each of these commands can operate in one of three modes:
Each of these commands can operate in one of four modes:
* apply change with a reboot (default): update configuration, reboot Talos node to apply configuration change
* apply change immediately (`--immediate` flag): change is applied immediately without a reboot, only `.cluster` sub-tree of the machine configuration can be updated in Talos 0.9
* apply change on next reboot (`--on-reboot`): change is staged to be applied after a reboot, but node is not rebooted
* apply change in automatic mode(default): reboot if the change can't be applied without a reboot, otherwise apply the change immediately
* apply change with a reboot (`--mode=reboot`): update configuration, reboot Talos node to apply configuration change
* apply change immediately (`--mode=no-reboot` flag): change is applied immediately without a reboot, fails if the change contains any fields that can not be updated without a reboot
* apply change on next reboot (`--mode=staged`): change is staged to be applied after a reboot, but node is not rebooted
* apply change in the interactive mode (`--mode=interactive`; only for `talosctl apply-config`): launches TUI based interactive installer
> Note: applying change on next reboot (`--on-reboot`) doesn't modify current node configuration, so next call to
> `talosctl edit machineconfig --on-reboot` will not see changes
> Note: applying change on next reboot (`--mode=staged`) doesn't modify current node configuration, so next call to
> `talosctl edit machineconfig --mode=staged` will not see changes
The list of config changes allowed to be applied immediately in talos v0.15:
* `.debug`
* `.cluster`
* `.machine.time`
* `.machine.certCANs`
* `.machine.network`
* `.machine.sysctls`
* `.machine.logging`
* `.machine.controlplane`
* `.machine.kubelet`
* `.machine.kernel`
### `talosctl apply-config`
@ -44,9 +59,17 @@ talosctl -n <IP> apply machineconfig -f config.yaml
Applying machine configuration immediately (without a reboot):
```bash
talosctl -n IP apply machineconfig -f config.yaml --immediate
talosctl -n IP apply machineconfig -f config.yaml --mode=no-reboot
```
Starting the interactive installer:
```bash
talosctl -n IP apply machineconfig --mode=interactive
```
> Note: when a Talos node is running in the maintenance mode it's necessary to provide `--insecure (-i)` flag to connect to the API and apply the config.
### `taloctl edit machineconfig`
Command `talosctl edit` loads current machine configuration from the node and launches configured editor to modify the config.
@ -70,14 +93,14 @@ talosctl -n <IP1>,<IP2>,... edit machineconfig
Applying machine configuration change immediately (without a reboot):
```bash
talosctl -n <IP> edit machineconfig --immediate
talosctl -n <IP> edit machineconfig --mode=no-reboot
```
### `talosctl patch machineconfig`
Command `talosctl patch` works similar to `talosctl edit` command - it loads current machine configuration, but instead of launching configured editor it applies [JSON patch](http://jsonpatch.com/) to the configuration and writes result back to the node.
Example, updating kubelet version (with a reboot):
Example, updating kubelet version (in auto mode):
```bash
$ talosctl -n <IP> patch machineconfig -p '[{"op": "replace", "path": "/machine/kubelet/image", "value": "ghcr.io/talos-systems/kubelet:v1.20.5"}]'
@ -87,18 +110,18 @@ patched mc at the node <IP>
Updating kube-apiserver version in immediate mode (without a reboot):
```bash
$ talosctl -n <IP> patch machineconfig --immediate -p '[{"op": "replace", "path": "/cluster/apiServer/image", "value": "k8s.gcr.io/kube-apiserver:v1.20.5"}]'
$ talosctl -n <IP> patch machineconfig --mode=no-reboot -p '[{"op": "replace", "path": "/cluster/apiServer/image", "value": "k8s.gcr.io/kube-apiserver:v1.20.5"}]'
patched mc at the node <IP>
```
Patch might be applied to multiple nodes when multiple IPs are specified:
```bash
taloctl -n <IP1>,<IP2>,... patch machineconfig --immediate -p '[{...}]'
taloctl -n <IP1>,<IP2>,... patch machineconfig -p '[{...}]'
```
### Recovering from Node Boot Failures
If a Talos node fails to boot because of wrong configuration (for example, control plane endpoint is incorrect), configuration can be updated to fix the issue.
If the boot sequence is still running, Talos might refuse applying config in default mode.
In that case `--on-reboot` mode can be used coupled with `talosctl reboot` command to trigger a reboot and apply configuration update.
In that case `--mode=staged` mode can be used coupled with `talosctl reboot` command to trigger a reboot and apply configuration update.

View File

@ -86,7 +86,7 @@ cluster:
secret: AbdsWjY9i797kGglghKvtGdxCsdllX9CemLq+WGVeaw=
```
> Note: This can be applied in immediate mode (no reboot required) by passing `--immediate` to either the `edit machineconfig` or `apply-config` subcommands.
> Note: This can be applied in immediate mode (no reboot required).
#### Talos v0.12

View File

@ -95,7 +95,7 @@ talosctl gen config my-cluster https://mycluster.local:6443 --config-patch '[{"o
Patching an existing node
```bash
talosctl patch --immediate machineconfig -n <node ip> --config-patch '[{"op": "add", "path": "/machine/sysctls", "value": {"vm.nr_hugepages": "1024"}}, {"op": "add", "path": "/machine/kubelet/extraArgs", "value": {"node-labels": "openebs.io/engine=mayastor"}}]'
talosctl patch --mode=no-reboot machineconfig -n <node ip> --config-patch '[{"op": "add", "path": "/machine/sysctls", "value": {"vm.nr_hugepages": "1024"}}, {"op": "add", "path": "/machine/kubelet/extraArgs", "value": {"node-labels": "openebs.io/engine=mayastor"}}]'
```
> Note: If you are adding/updating the `vm.nr_hugepages` on a node which already had the `openebs.io/engine=mayastor` label set, you'd need to restart kubelet so that it picks up the new value, by issuing the following command

View File

@ -293,13 +293,13 @@ talosctl --nodes <master node> kubeconfig
Patch machine configuration using `talosctl patch` command:
```bash
$ talosctl -n <CONTROL_PLANE_IP_1> patch mc --immediate -p '[{"op": "replace", "path": "/cluster/apiServer/image", "value": "k8s.gcr.io/kube-apiserver:v1.20.4"}]'
$ talosctl -n <CONTROL_PLANE_IP_1> patch mc --mode=no-reboot -p '[{"op": "replace", "path": "/cluster/apiServer/image", "value": "k8s.gcr.io/kube-apiserver:v1.20.4"}]'
patched mc at the node 172.20.0.2
```
JSON patch might need to be adjusted if current machine configuration is missing `.cluster.apiServer.image` key.
Also machine configuration can be edited manually with `talosctl -n <IP> edit mc --immediate`.
Also machine configuration can be edited manually with `talosctl -n <IP> edit mc --mode=no-reboot`.
Capture new version of `kube-apiserver` config with:
@ -347,7 +347,7 @@ Repeat this process for every control plane node, verifying that state got propa
Patch machine configuration using `talosctl patch` command:
```bash
$ talosctl -n <CONTROL_PLANE_IP_1> patch mc --immediate -p '[{"op": "replace", "path": "/cluster/controllerManager/image", "value": "k8s.gcr.io/kube-controller-manager:v1.20.4"}]'
$ talosctl -n <CONTROL_PLANE_IP_1> patch mc --mode=no-reboot -p '[{"op": "replace", "path": "/cluster/controllerManager/image", "value": "k8s.gcr.io/kube-controller-manager:v1.20.4"}]'
patched mc at the node 172.20.0.2
```
@ -396,7 +396,7 @@ Repeat this process for every control plane node, verifying that state got propa
Patch machine configuration using `talosctl patch` command:
```bash
$ talosctl -n <CONTROL_PLANE_IP_1> patch mc --immediate -p '[{"op": "replace", "path": "/cluster/scheduler/image", "value": "k8s.gcr.io/kube-scheduler:v1.20.4"}]'
$ talosctl -n <CONTROL_PLANE_IP_1> patch mc --mode=no-reboot -p '[{"op": "replace", "path": "/cluster/scheduler/image", "value": "k8s.gcr.io/kube-scheduler:v1.20.4"}]'
patched mc at the node 172.20.0.2
```
@ -509,7 +509,7 @@ kubectl apply -f manifests.yaml
For every node, patch machine configuration with new kubelet version, wait for the kubelet to restart with new version:
```bash
$ talosctl -n <IP> patch mc --immediate -p '[{"op": "replace", "path": "/machine/kubelet/image", "value": "ghcr.io/talos-systems/kubelet:v1.23.0"}]'
$ talosctl -n <IP> patch mc --mode=no-reboot -p '[{"op": "replace", "path": "/machine/kubelet/image", "value": "ghcr.io/talos-systems/kubelet:v1.23.0"}]'
patched mc at the node 172.20.0.2
```

View File

@ -291,7 +291,7 @@ Talos will print them out during the boot process:
[ 4.614985] [talos] task loadConfig (1/1): upload configuration using talosctl:
[ 4.616978] [talos] task loadConfig (1/1): talosctl apply-config --insecure --nodes 192.168.0.2 --file <config.yaml>
[ 4.620168] [talos] task loadConfig (1/1): or apply configuration using talosctl interactive installer:
[ 4.623046] [talos] task loadConfig (1/1): talosctl apply-config --insecure --nodes 192.168.0.2 --interactive
[ 4.623046] [talos] task loadConfig (1/1): talosctl apply-config --insecure --nodes 192.168.0.2 --mode=interactive
[ 4.626365] [talos] task loadConfig (1/1): optionally with node fingerprint check:
[ 4.628692] [talos] task loadConfig (1/1): talosctl apply-config --insecure --nodes 192.168.0.2 --cert-fingerprint 'xA9a1t2dMxB0NJ0qH1pDzilWbA3+DK/DjVbFaJBYheE=' --file <config.yaml>
```

View File

@ -8,7 +8,7 @@ The new implementation is still using the same machine configuration file format
in the way Talos works in 0.11.
The most notable change in Talos 0.11 is that all changes to machine configuration `.machine.network` can be applied now in immediate mode (without a reboot) via
`talosctl edit mc --immediate` or `talosctl apply-config --immediate`.
`talosctl edit mc --mode=no-reboot` or `talosctl apply-config --mode=no-reboot`.
## Resources

View File

@ -157,6 +157,7 @@ description: Talos gRPC API reference.
- [VersionInfo](#machine.VersionInfo)
- [VersionResponse](#machine.VersionResponse)
- [ApplyConfigurationRequest.Mode](#machine.ApplyConfigurationRequest.Mode)
- [ListRequest.Type](#machine.ListRequest.Type)
- [MachineConfig.MachineType](#machine.MachineConfig.MachineType)
- [PhaseEvent.Action](#machine.PhaseEvent.Action)
@ -483,6 +484,8 @@ ApplyConfigurationResponse describes the response to a configuration request.
| ----- | ---- | ----- | ----------- |
| metadata | [common.Metadata](#common.Metadata) | | |
| warnings | [string](#string) | repeated | Configuration validation warnings. |
| mode | [ApplyConfigurationRequest.Mode](#machine.ApplyConfigurationRequest.Mode) | | States which mode was actually chosen. |
| mode_details | [string](#string) | | Human-readable message explaining the result of the apply configuration call. |
@ -500,8 +503,9 @@ node.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| data | [bytes](#bytes) | | |
| on_reboot | [bool](#bool) | | |
| immediate | [bool](#bool) | | |
| on_reboot | [bool](#bool) | | **Deprecated.** replaced by mode |
| immediate | [bool](#bool) | | **Deprecated.** replaced by mode |
| mode | [ApplyConfigurationRequest.Mode](#machine.ApplyConfigurationRequest.Mode) | | |
@ -2648,6 +2652,20 @@ rpc upgrade
<!-- end messages -->
<a name="machine.ApplyConfigurationRequest.Mode"></a>
### ApplyConfigurationRequest.Mode
| Name | Number | Description |
| ---- | ------ | ----------- |
| REBOOT | 0 | |
| AUTO | 1 | |
| NO_REBOOT | 2 | |
| STAGED | 3 | |
<a name="machine.ListRequest.Type"></a>
### ListRequest.Type

View File

@ -16,13 +16,11 @@ talosctl apply-config [flags]
### Options
```
--cert-fingerprint strings list of server certificate fingeprints to accept (defaults to no check)
-f, --file string the filename of the updated configuration
-h, --help help for apply-config
--immediate apply the config immediately (without a reboot)
-i, --insecure apply the config using the insecure (encrypted with no auth) maintenance service
--interactive apply the config using text based interactive mode
--on-reboot apply the config on reboot
--cert-fingerprint strings list of server certificate fingeprints to accept (defaults to no check)
-f, --file string the filename of the updated configuration
-h, --help help for apply-config
-i, --insecure apply the config using the insecure (encrypted with no auth) maintenance service
-m, --mode auto, interactive, no-reboot, reboot, staged apply config mode (default auto)
```
### Options inherited from parent commands
@ -816,10 +814,9 @@ talosctl edit <type> [<id>] [flags]
### Options
```
-h, --help help for edit
--immediate apply the change immediately (without a reboot)
--namespace string resource namespace (default is to use default namespace per resource)
--on-reboot apply the change on next reboot
-h, --help help for edit
-m, --mode auto, no-reboot, reboot, staged apply config mode (default auto)
--namespace string resource namespace (default is to use default namespace per resource)
```
### Options inherited from parent commands
@ -1588,12 +1585,11 @@ talosctl patch <type> [<id>] [flags]
### Options
```
-h, --help help for patch
--immediate apply the change immediately (without a reboot)
--namespace string resource namespace (default is to use default namespace per resource)
--on-reboot apply the change on next reboot
-p, --patch string the patch to be applied to the resource file.
--patch-file string a file containing a patch to be applied to the resource.
-h, --help help for patch
-m, --mode auto, no-reboot, reboot, staged apply config mode (default auto)
--namespace string resource namespace (default is to use default namespace per resource)
-p, --patch string the patch to be applied to the resource file.
--patch-file string a file containing a patch to be applied to the resource.
```
### Options inherited from parent commands

View File

@ -43,7 +43,7 @@ Insert the SD card to your board, turn it on and wait for the console to show yo
Following the instructions in the console output to connect to the interactive installer:
```bash
talosctl apply-config --insecure --interactive --nodes <node IP or DNS name>
talosctl apply-config --insecure --mode=interactive --nodes <node IP or DNS name>
```
Once the interactive installation is applied, the cluster will form and you can then use `kubectl`.

View File

@ -43,7 +43,7 @@ Insert the SD card to your board, turn it on and wait for the console to show yo
Following the instructions in the console output to connect to the interactive installer:
```bash
talosctl apply-config --insecure --interactive --nodes <node IP or DNS name>
talosctl apply-config --insecure --mode=interactive --nodes <node IP or DNS name>
```
Once the interactive installation is applied, the cluster will form and you can then use `kubectl`.

View File

@ -43,7 +43,7 @@ Insert the SD card to your board, turn it on and wait for the console to show yo
Following the instructions in the console output to connect to the interactive installer:
```bash
talosctl apply-config --insecure --interactive --nodes <node IP or DNS name>
talosctl apply-config --insecure --mode=interactive --nodes <node IP or DNS name>
```
Once the interactive installation is applied, the cluster will form and you can then use `kubectl`.

View File

@ -43,7 +43,7 @@ Insert the SD card to your board, turn it on and wait for the console to show yo
Following the instructions in the console output to connect to the interactive installer:
```bash
talosctl apply-config --insecure --interactive --nodes <node IP or DNS name>
talosctl apply-config --insecure --mode=interactive --nodes <node IP or DNS name>
```
Once the interactive installation is applied, the cluster will form and you can then use `kubectl`.

View File

@ -43,7 +43,7 @@ Insert the SD card to your board, turn it on and wait for the console to show yo
Following the instructions in the console output to connect to the interactive installer:
```bash
talosctl apply-config --insecure --interactive --nodes <node IP or DNS name>
talosctl apply-config --insecure --mode=interactive --nodes <node IP or DNS name>
```
Once the interactive installation is applied, the cluster will form and you can then use `kubectl`.

View File

@ -71,7 +71,7 @@ Insert the SD card to your board, turn it on and wait for the console to show yo
Following the instructions in the console output to connect to the interactive installer:
```bash
talosctl apply-config --insecure --interactive --nodes <node IP or DNS name>
talosctl apply-config --insecure --mode=interactive --nodes <node IP or DNS name>
```
Once the interactive installation is applied, the cluster will form and you can then use `kubectl`.