Skip to content

Commit c12de9a

Browse files
authored
feat(nodeadm): add nodeadm config dump command (#2107)
* feat(nodeadm): add config dump command * refactor(nodeadm): move resolveConfig to cli package
1 parent 21ead44 commit c12de9a

File tree

6 files changed

+163
-77
lines changed

6 files changed

+163
-77
lines changed

nodeadm/cmd/nodeadm/config/dump.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/awslabs/amazon-eks-ami/nodeadm/api/v1alpha1"
8+
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/api/bridge"
9+
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/cli"
10+
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/util"
11+
"github.com/integrii/flaggy"
12+
"go.uber.org/zap"
13+
)
14+
15+
type dumpCmd struct {
16+
cmd *flaggy.Subcommand
17+
18+
configSources []string
19+
configCache string
20+
configOutput string
21+
}
22+
23+
func NewDumpCommand() cli.Command {
24+
c := dumpCmd{}
25+
c.cmd = flaggy.NewSubcommand("dump")
26+
c.cmd.Description = "Dump configuration"
27+
cli.RegisterFlagConfigSources(c.cmd, &c.configSources)
28+
cli.RegisterFlagConfigCache(c.cmd, &c.configCache)
29+
cli.RegisterFlagConfigOutput(c.cmd, &c.configOutput)
30+
return &c
31+
}
32+
33+
func (c *dumpCmd) Flaggy() *flaggy.Subcommand {
34+
return c.cmd
35+
}
36+
37+
func (c *dumpCmd) Run(log *zap.Logger, opts *cli.GlobalOptions) error {
38+
c.configSources = cli.ResolveConfigSources(c.configSources)
39+
40+
if c.configOutput != "" {
41+
log.Info("Dumping configuration", zap.Strings("source", c.configSources), zap.String("output", c.configOutput))
42+
}
43+
44+
nodeConfig, _, _, err := cli.ResolveConfig(log, c.configSources, c.configCache)
45+
if err != nil {
46+
return err
47+
}
48+
49+
data, err := bridge.EncodeNodeConfig(nodeConfig, v1alpha1.GroupVersion)
50+
if err != nil {
51+
return fmt.Errorf("failed to encode config: %w", err)
52+
}
53+
54+
if c.configOutput != "" {
55+
if err := util.WriteFileWithDir(c.configOutput, data, 0644); err != nil {
56+
return fmt.Errorf("failed to write config to file: %w", err)
57+
}
58+
log.Info("Configuration dumped")
59+
return nil
60+
}
61+
62+
if _, err := os.Stdout.Write(data); err != nil {
63+
return fmt.Errorf("failed to write config to stdout: %w", err)
64+
}
65+
return nil
66+
}

nodeadm/cmd/nodeadm/config/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ import (
77
func NewConfigCommand() cli.Command {
88
container := cli.NewCommandContainer("config", "Manage configuration")
99
container.AddCommand(NewCheckCommand())
10+
container.AddCommand(NewDumpCommand())
1011
return container.AsCommand()
1112
}

nodeadm/cmd/nodeadm/init/init.go

Lines changed: 3 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ package init
22

33
import (
44
"context"
5-
"errors"
6-
"os"
7-
"reflect"
85
"time"
96

107
"github.com/aws/aws-sdk-go-v2/aws"
@@ -15,15 +12,12 @@ import (
1512
"k8s.io/utils/strings/slices"
1613

1714
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/api"
18-
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/api/bridge"
1915
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/aws/imds"
2016
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/cli"
21-
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/configprovider"
2217
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/containerd"
2318
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/daemon"
2419
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/kubelet"
2520
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/system"
26-
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/util"
2721
)
2822

2923
const (
@@ -38,7 +32,7 @@ func NewInitCommand() cli.Command {
3832
c.cmd.Description = "Initialize this instance as a node in an EKS cluster"
3933
c.cmd.StringSlice(&c.daemons, "d", "daemon", "specify one or more of `containerd` and `kubelet`. This is intended for testing and should not be used in a production environment.")
4034
c.cmd.StringSlice(&c.skipPhases, "s", "skip", "phases of the bootstrap you want to skip")
41-
c.cmd.String(&c.configCache, "", "config-cache", "File path at which to cache the resolved/enriched config. This can make repeated init calls more efficient. JSON encoding will be used.")
35+
cli.RegisterFlagConfigCache(c.cmd, &c.configCache)
4236
cli.RegisterFlagConfigSources(c.cmd, &c.configSources)
4337
return &c
4438
}
@@ -70,7 +64,7 @@ func (c *initCmd) Run(log *zap.Logger, opts *cli.GlobalOptions) error {
7064

7165
log.Info("Loading configuration..", zap.Strings("configSource", c.configSources), zap.String("configCache", c.configCache))
7266

73-
nodeConfig, isChanged, shouldEnrichConfig, err := c.resolveConfig(log)
67+
nodeConfig, isChanged, shouldEnrichConfig, err := cli.ResolveConfig(log, c.configSources, c.configCache)
7468
if err != nil {
7569
return err
7670
}
@@ -137,7 +131,7 @@ func (c *initCmd) Run(log *zap.Logger, opts *cli.GlobalOptions) error {
137131
}
138132

139133
// this is not fatal, so do not use a blocking error.
140-
if err := saveCachedConfig(nodeConfig, c.configCache); err != nil {
134+
if err := cli.SaveCachedConfig(nodeConfig, c.configCache); err != nil {
141135
log.Error("Failed to cache config", zap.String("configCache", c.configCache), zap.Error(err))
142136
}
143137
}
@@ -162,52 +156,6 @@ func (c *initCmd) Run(log *zap.Logger, opts *cli.GlobalOptions) error {
162156
return nil
163157
}
164158

165-
// resolveConfig returns either the cached config or the provided config chain.
166-
func (c *initCmd) resolveConfig(log *zap.Logger) (cfg *api.NodeConfig, isChanged bool, shouldEnrichConfig bool, err error) {
167-
var cachedConfig *api.NodeConfig
168-
shouldEnrichConfig = false
169-
if len(c.configCache) > 0 {
170-
config, err := loadCachedConfig(c.configCache)
171-
if err != nil {
172-
log.Warn("failed to load cached config", zap.Error(err))
173-
} else {
174-
cachedConfig = config
175-
}
176-
}
177-
178-
provider, err := configprovider.BuildConfigProviderChain(c.configSources)
179-
if err != nil {
180-
return nil, false, shouldEnrichConfig, err
181-
}
182-
nodeConfig, err := provider.Provide()
183-
// if the error is just that no config is provided, then attempt to use the
184-
// cached config as a fallback. otherwise, treat this as a fatal error.
185-
if errors.Is(err, configprovider.ErrNoConfigInChain) && cachedConfig != nil {
186-
log.Warn("Falling back to cached config...")
187-
return cachedConfig, false, shouldEnrichConfig, nil
188-
} else if err != nil {
189-
return nil, false, shouldEnrichConfig, err
190-
}
191-
192-
// if the cached and the provider config specs are the same, we'll just
193-
// use the cached spec because it also has the internal NodeConfig
194-
// .status information cached.
195-
//
196-
// if perf of reflect.DeepEqual becomes an issue, look into something like: https://github.com/Wind-River/deepequal-gen
197-
if cachedConfig != nil && reflect.DeepEqual(nodeConfig.Spec, cachedConfig.Spec) {
198-
return cachedConfig, false, shouldEnrichConfig, nil
199-
}
200-
201-
// If the code reaches here it means that either no-config is cached (isChanged = false)
202-
// Or the cache exists and the cached spec does not match the node spec (isChanged = true)
203-
// In both cases, the config should be enriched
204-
shouldEnrichConfig = true
205-
206-
// we return the presence of a cache as the `isChanged` value, because if we
207-
// had a cache hit and didnt use it, it's because we have a modified config.
208-
return nodeConfig, cachedConfig != nil, shouldEnrichConfig, nil
209-
}
210-
211159
// enrichConfig populates the internal .status portion of the NodeConfig, used
212160
// only for internal implementation details.
213161
func (*initCmd) enrichConfig(log *zap.Logger, cfg *api.NodeConfig, opts *cli.GlobalOptions) error {
@@ -257,24 +205,6 @@ func (*initCmd) enrichConfig(log *zap.Logger, cfg *api.NodeConfig, opts *cli.Glo
257205
return nil
258206
}
259207

260-
func loadCachedConfig(path string) (*api.NodeConfig, error) {
261-
// #nosec G304 // intended mechanism to read user-provided config file
262-
nodeConfigData, err := os.ReadFile(path)
263-
if err != nil {
264-
return nil, err
265-
}
266-
gvk := bridge.InternalGroupVersion.WithKind(api.KindNodeConfig)
267-
return bridge.DecodeNodeConfig(nodeConfigData, &gvk)
268-
}
269-
270-
func saveCachedConfig(cfg *api.NodeConfig, path string) error {
271-
data, err := bridge.EncodeNodeConfig(cfg)
272-
if err != nil {
273-
return err
274-
}
275-
return util.WriteFileWithDir(path, data, 0644)
276-
}
277-
278208
func (c *initCmd) configureDaemons(log *zap.Logger, cfg *api.NodeConfig, daemons []daemon.Daemon) error {
279209
for _, daemon := range daemons {
280210
if len(c.daemons) > 0 && !slices.Contains(c.daemons, daemon.Name()) {

nodeadm/internal/api/bridge/encode.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
// EncodeNodeConfig marshals the given internal NodeConfig object to JSON.
1212
// JSON is used because it's simply easier than YAML to work with in scripting contexts.
13-
func EncodeNodeConfig(nodeConfig *internalapi.NodeConfig) ([]byte, error) {
13+
func EncodeNodeConfig(nodeConfig *internalapi.NodeConfig, groupVersioner runtime.GroupVersioner) ([]byte, error) {
1414
scheme := runtime.NewScheme()
1515
err := localSchemeBuilder.AddToScheme(scheme)
1616
if err != nil {
@@ -21,7 +21,12 @@ func EncodeNodeConfig(nodeConfig *internalapi.NodeConfig) ([]byte, error) {
2121
if !matched {
2222
return nil, fmt.Errorf("JSON did not match any supported media type")
2323
}
24-
// always encode to the internal version so we don't lose any internal state
25-
codec := codecs.EncoderForVersion(info.Serializer, InternalGroupVersion)
24+
codec := codecs.EncoderForVersion(info.Serializer, groupVersioner)
2625
return runtime.Encode(codec, nodeConfig)
2726
}
27+
28+
// EncodeInternalNodeConfig marshals the given internal NodeConfig object to JSON using the internal group version.
29+
// JSON is used because it's simply easier than YAML to work with in scripting contexts.
30+
func EncodeInternalNodeConfig(nodeConfig *internalapi.NodeConfig) ([]byte, error) {
31+
return EncodeNodeConfig(nodeConfig, InternalGroupVersion)
32+
}

nodeadm/internal/cli/config.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package cli
2+
3+
import (
4+
"errors"
5+
"os"
6+
"reflect"
7+
8+
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/api"
9+
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/api/bridge"
10+
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/util"
11+
12+
"github.com/awslabs/amazon-eks-ami/nodeadm/internal/configprovider"
13+
14+
"go.uber.org/zap"
15+
)
16+
17+
// ResolveConfig returns either the cached config or the provided config chain.
18+
func ResolveConfig(log *zap.Logger, rawConfigSourceURLs []string, configCachePath string) (cfg *api.NodeConfig, isChanged bool, shouldEnrichConfig bool, err error) {
19+
var cachedConfig *api.NodeConfig
20+
shouldEnrichConfig = false
21+
22+
if len(configCachePath) > 0 {
23+
config, err := LoadCachedConfig(configCachePath)
24+
if err != nil {
25+
log.Warn("failed to load cached config", zap.Error(err))
26+
} else {
27+
cachedConfig = config
28+
}
29+
}
30+
31+
provider, err := configprovider.BuildConfigProviderChain(rawConfigSourceURLs)
32+
if err != nil {
33+
return nil, false, shouldEnrichConfig, err
34+
}
35+
nodeConfig, err := provider.Provide()
36+
// if the error is just that no config is provided, then attempt to use the
37+
// cached config as a fallback. otherwise, treat this as a fatal error.
38+
if errors.Is(err, configprovider.ErrNoConfigInChain) && cachedConfig != nil {
39+
log.Warn("Falling back to cached config...")
40+
return cachedConfig, false, shouldEnrichConfig, nil
41+
} else if err != nil {
42+
return nil, false, shouldEnrichConfig, err
43+
}
44+
45+
// if the cached and the provider config specs are the same, we'll just
46+
// use the cached spec because it also has the internal NodeConfig
47+
// .status information cached.
48+
//
49+
// if perf of reflect.DeepEqual becomes an issue, look into something like: https://github.com/Wind-River/deepequal-gen
50+
if cachedConfig != nil && reflect.DeepEqual(nodeConfig.Spec, cachedConfig.Spec) {
51+
return cachedConfig, false, shouldEnrichConfig, nil
52+
}
53+
54+
// If the code reaches here it means that either no-config is cached (isChanged = false)
55+
// Or the cache exists and the cached spec does not match the node spec (isChanged = true)
56+
// In both cases, the config should be enriched
57+
shouldEnrichConfig = true
58+
59+
// we return the presence of a cache as the `isChanged` value, because if we
60+
// had a cache hit and didnt use it, it's because we have a modified config.
61+
return nodeConfig, cachedConfig != nil, shouldEnrichConfig, nil
62+
}
63+
64+
func LoadCachedConfig(path string) (*api.NodeConfig, error) {
65+
// #nosec G304 // intended mechanism to read user-provided config file
66+
nodeConfigData, err := os.ReadFile(path)
67+
if err != nil {
68+
return nil, err
69+
}
70+
gvk := bridge.InternalGroupVersion.WithKind(api.KindNodeConfig)
71+
return bridge.DecodeNodeConfig(nodeConfigData, &gvk)
72+
}
73+
74+
func SaveCachedConfig(cfg *api.NodeConfig, path string) error {
75+
data, err := bridge.EncodeInternalNodeConfig(cfg)
76+
if err != nil {
77+
return err
78+
}
79+
return util.WriteFileWithDir(path, data, 0644)
80+
}

nodeadm/internal/cli/options.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func NewGlobalOptions() *GlobalOptions {
2626
}
2727

2828
func RegisterFlagConfigOutput(c *flaggy.Subcommand, configOutput *string) {
29-
c.String(configOutput, "", "config-output", "File path to write the final resolved and enriched config to. JSON encoding is used.")
29+
c.String(configOutput, "o", "config-output", "File path to write the final resolved config to. JSON encoding is used.")
3030
}
3131

3232
// RegisterFlagConfigSources maps a command-line flag for config sources to the specified string slice for the specified command.
@@ -45,3 +45,7 @@ func ResolveConfigSources(configSources []string) []string {
4545
zap.L().Info("Using default config sources...")
4646
return DefaultConfigSources
4747
}
48+
49+
func RegisterFlagConfigCache(c *flaggy.Subcommand, configCache *string) {
50+
c.String(configCache, "", "config-cache", "File path at which to cache the resolved/enriched config. This can make repeated init calls more efficient. JSON encoding will be used.")
51+
}

0 commit comments

Comments
 (0)