Skip to content

Commit f7cedae

Browse files
feat: apply config overrides during node join (#243)
starts to apply patches to both k0s' config `api` and `storage` properties. patches are received from the kots api and applied in the following order: embedded overrides -> user provided. this commit also adds unit tests for the new functionality and to the old pathing functions. this is a overview of the changes present on this commit: - added error treatment in case of failure on override apply. - removed some empty lines. - added two new fields to the join token response struct. - created function to apply overrides in place (/etc/k0s/k0s.yaml). - add unit tests for multiple override apply functions. - create an abstraction to make reading embed data easier.
1 parent 05247d0 commit f7cedae

31 files changed

+1005
-67
lines changed

cmd/embedded-cluster/install.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -145,38 +145,36 @@ func createK0sctlConfigBackup(ctx context.Context) error {
145145
// updateConfig updates the k0sctl.yaml file with the latest configuration
146146
// options.
147147
func updateConfig(c *cli.Context) error {
148-
149148
if err := createK0sctlConfigBackup(c.Context); err != nil {
150149
return fmt.Errorf("unable to create config backup: %w", err)
151150
}
152-
153151
cfgpath := defaults.PathToConfig("k0sctl.yaml")
154152
cfg, err := config.ReadConfigFile(cfgpath)
155153
if err != nil {
156154
return fmt.Errorf("unable to read cluster config: %w", err)
157155
}
158156
cfg.Spec.K0s.Version = k0sversion.MustParse(defaults.K0sVersion)
159-
160157
if c.String("overrides") != "" {
161158
eucfg, err := parseEndUserConfig(c.String("overrides"))
162159
if err != nil {
163160
return fmt.Errorf("unable to process overrides file: %w", err)
164161
}
165-
config.ApplyEmbeddedUnsupportedOverrides(cfg, eucfg.Spec.UnsupportedOverrides.K0s)
162+
if err := config.ApplyEmbeddedUnsupportedOverrides(
163+
cfg, eucfg.Spec.UnsupportedOverrides.K0s,
164+
); err != nil {
165+
return fmt.Errorf("unable to apply overrides: %w", err)
166+
}
166167
}
167-
168168
opts := []addons.Option{}
169169
if c.Bool("no-prompt") {
170170
opts = append(opts, addons.WithoutPrompt())
171171
}
172172
for _, addon := range c.StringSlice("disable-addon") {
173173
opts = append(opts, addons.WithoutAddon(addon))
174174
}
175-
176175
if err := config.UpdateHelmConfigs(cfg, opts...); err != nil {
177176
return fmt.Errorf("unable to update helm configs: %w", err)
178177
}
179-
180178
fp, err := os.OpenFile(cfgpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
181179
if err != nil {
182180
return fmt.Errorf("unable to create config file: %w", err)
@@ -247,33 +245,31 @@ func ensureK0sctlConfig(c *cli.Context, useprompt bool) error {
247245
} else if !os.IsNotExist(err) {
248246
return fmt.Errorf("unable to open config: %w", err)
249247
}
250-
251248
cfg, err := config.RenderClusterConfig(c.Context, multi)
252249
if err != nil {
253250
return fmt.Errorf("unable to render config: %w", err)
254251
}
255-
256252
if c.String("overrides") != "" {
257253
eucfg, err := parseEndUserConfig(c.String("overrides"))
258254
if err != nil {
259255
return fmt.Errorf("unable to process overrides file: %w", err)
260256
}
261-
config.ApplyEmbeddedUnsupportedOverrides(cfg, eucfg.Spec.UnsupportedOverrides.K0s)
257+
if err := config.ApplyEmbeddedUnsupportedOverrides(
258+
cfg, eucfg.Spec.UnsupportedOverrides.K0s,
259+
); err != nil {
260+
return fmt.Errorf("unable to apply overrides: %w", err)
261+
}
262262
}
263-
264263
opts := []addons.Option{}
265264
if c.Bool("no-prompt") {
266265
opts = append(opts, addons.WithoutPrompt())
267266
}
268-
269267
for _, addon := range c.StringSlice("disable-addon") {
270268
opts = append(opts, addons.WithoutAddon(addon))
271269
}
272-
273270
if err := config.UpdateHelmConfigs(cfg, opts...); err != nil {
274271
return fmt.Errorf("unable to update helm configs: %w", err)
275272
}
276-
277273
fp, err := os.OpenFile(cfgpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
278274
if err != nil {
279275
return fmt.Errorf("unable to create config file: %w", err)

cmd/embedded-cluster/join.go

Lines changed: 115 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ import (
1414
"time"
1515

1616
"github.com/google/uuid"
17+
"github.com/k0sproject/dig"
18+
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster"
1719
"github.com/sirupsen/logrus"
1820
"github.com/urfave/cli/v2"
21+
"gopkg.in/yaml.v2"
1922

2023
"github.com/replicatedhq/embedded-cluster/pkg/addons"
24+
"github.com/replicatedhq/embedded-cluster/pkg/config"
2125
"github.com/replicatedhq/embedded-cluster/pkg/defaults"
2226
"github.com/replicatedhq/embedded-cluster/pkg/goods"
2327
"github.com/replicatedhq/embedded-cluster/pkg/metrics"
@@ -26,12 +30,43 @@ import (
2630
"github.com/replicatedhq/embedded-cluster/pkg/prompts"
2731
)
2832

29-
// JoinCommandResponse is the response from the kots api we use to fetch the k0s join
30-
// token. It returns the actual command we need to run and also the cluster ID.
33+
// JoinCommandResponse is the response from the kots api we use to fetch the k0s join token.
3134
type JoinCommandResponse struct {
32-
K0sJoinCommand string `json:"k0sJoinCommand"`
33-
K0sToken string `json:"k0sToken"`
34-
ClusterID uuid.UUID `json:"clusterID"`
35+
K0sJoinCommand string `json:"k0sJoinCommand"`
36+
K0sToken string `json:"k0sToken"`
37+
ClusterID uuid.UUID `json:"clusterID"`
38+
K0sUnsupportedOverrides string `json:"k0sUnsupportedOverrides"`
39+
EndUserK0sConfigOverrides string `json:"endUserK0sConfigOverrides"`
40+
}
41+
42+
// extractK0sConfigOverridePatch parses the provided override and returns a dig.Mapping that
43+
// can be then applied on top a k0s configuration file to set both `api` and `storage` spec
44+
// fields. All other fields in the override are ignored.
45+
func (j JoinCommandResponse) extractK0sConfigOverridePatch(data []byte) (dig.Mapping, error) {
46+
config := dig.Mapping{}
47+
if err := yaml.Unmarshal(data, &config); err != nil {
48+
return nil, fmt.Errorf("unable to unmarshal embedded config: %w", err)
49+
}
50+
result := dig.Mapping{}
51+
if api := config.DigMapping("config", "spec", "api"); len(api) > 0 {
52+
result.DigMapping("config", "spec")["api"] = api
53+
}
54+
if storage := config.DigMapping("config", "spec", "storage"); len(storage) > 0 {
55+
result.DigMapping("config", "spec")["storage"] = storage
56+
}
57+
return result, nil
58+
}
59+
60+
// EndUserOverrides returns a dig.Mapping that can be applied on top of a k0s configuration.
61+
// This patch is assembled based on the EndUserK0sConfigOverrides field.
62+
func (j JoinCommandResponse) EndUserOverrides() (dig.Mapping, error) {
63+
return j.extractK0sConfigOverridePatch([]byte(j.EndUserK0sConfigOverrides))
64+
}
65+
66+
// EmbeddedOverrides returns a dig.Mapping that can be applied on top of a k0s configuration.
67+
// This patch is assembled based on the K0sUnsupportedOverrides field.
68+
func (j JoinCommandResponse) EmbeddedOverrides() (dig.Mapping, error) {
69+
return j.extractK0sConfigOverridePatch([]byte(j.K0sUnsupportedOverrides))
3570
}
3671

3772
// getJoinToken issues a request to the kots api to get the actual join command
@@ -108,6 +143,12 @@ var joinCommand = &cli.Command{
108143
metrics.ReportJoinFailed(c.Context, jcmd.ClusterID, err)
109144
return err
110145
}
146+
loading.Infof("Applying configuration overrides")
147+
if err := applyJoinConfigurationOverrides(c, jcmd); err != nil {
148+
err := fmt.Errorf("unable to apply configuration overrides: %w", err)
149+
metrics.ReportJoinFailed(c.Context, jcmd.ClusterID, err)
150+
return err
151+
}
111152
loading.Infof("Creating systemd unit file")
112153
if err := createSystemdUnitFile(jcmd.K0sJoinCommand); err != nil {
113154
err := fmt.Errorf("unable to create systemd unit file: %w", err)
@@ -133,6 +174,75 @@ var joinCommand = &cli.Command{
133174
},
134175
}
135176

177+
// applyJoinConfigurationOverrides applies both config overrides received from the kots api.
178+
// Applies first the EmbeddedOverrides and then the EndUserOverrides.
179+
func applyJoinConfigurationOverrides(c *cli.Context, jcmd *JoinCommandResponse) error {
180+
patch, err := jcmd.EmbeddedOverrides()
181+
if err != nil {
182+
return fmt.Errorf("unable to get embedded overrides: %w", err)
183+
}
184+
if len(patch) > 0 {
185+
if data, err := yaml.Marshal(patch); err != nil {
186+
return fmt.Errorf("unable to marshal embedded overrides: %w", err)
187+
} else if err := patchK0sConfig("/etc/k0s/k0s.yaml", string(data)); err != nil {
188+
return fmt.Errorf("unable to patch config with embedded data: %w", err)
189+
}
190+
}
191+
if patch, err = jcmd.EndUserOverrides(); err != nil {
192+
return fmt.Errorf("unable to get embedded overrides: %w", err)
193+
} else if len(patch) == 0 {
194+
return nil
195+
}
196+
if data, err := yaml.Marshal(patch); err != nil {
197+
return fmt.Errorf("unable to marshal embedded overrides: %w", err)
198+
} else if err := patchK0sConfig("/etc/k0s/k0s.yaml", string(data)); err != nil {
199+
return fmt.Errorf("unable to patch config with embedded data: %w", err)
200+
}
201+
return nil
202+
}
203+
204+
// patchK0sConfig patches the created k0s config with the unsupported overrides passed in.
205+
func patchK0sConfig(path string, patch string) error {
206+
if len(patch) == 0 {
207+
return nil
208+
}
209+
finalcfg := dig.Mapping{
210+
"apiVersion": "k0s.k0sproject.io/v1beta1",
211+
"kind": "ClusterConfig",
212+
"metadata": dig.Mapping{"name": defaults.BinaryName()},
213+
}
214+
if _, err := os.Stat(path); err == nil {
215+
data, err := os.ReadFile(path)
216+
if err != nil {
217+
return fmt.Errorf("unable to read node config: %w", err)
218+
}
219+
finalcfg = dig.Mapping{}
220+
if err := yaml.Unmarshal(data, &finalcfg); err != nil {
221+
return fmt.Errorf("unable to unmarshal node config: %w", err)
222+
}
223+
}
224+
k0sconfig := cluster.K0s{Config: finalcfg.Dup()}
225+
result, err := config.PatchK0sConfig(&k0sconfig, patch)
226+
if err != nil {
227+
return fmt.Errorf("unable to patch node config: %w", err)
228+
}
229+
if len(result.Config.DigMapping("spec", "api")) > 0 {
230+
finalcfg.DigMapping("spec")["api"] = result.Config.DigMapping("spec", "api")
231+
}
232+
if len(result.Config.DigMapping("spec", "storage")) > 0 {
233+
finalcfg.DigMapping("spec")["storage"] = result.Config.DigMapping("spec", "storage")
234+
}
235+
out, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
236+
if err != nil {
237+
return fmt.Errorf("unable to open node config file for writing: %w", err)
238+
}
239+
defer out.Close()
240+
if err := yaml.NewEncoder(out).Encode(finalcfg); err != nil {
241+
return fmt.Errorf("unable to write node config: %w", err)
242+
}
243+
return nil
244+
}
245+
136246
// saveTokenToDisk saves the provided token in "/etc/k0s/join-token".
137247
func saveTokenToDisk(token string) error {
138248
if err := os.MkdirAll("/etc/k0s", 0755); err != nil {

cmd/embedded-cluster/join_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package main
2+
3+
import (
4+
"embed"
5+
"fmt"
6+
"os"
7+
"path"
8+
"strings"
9+
"testing"
10+
11+
"github.com/k0sproject/dig"
12+
embeddedclusterv1beta1 "github.com/replicatedhq/embedded-cluster-operator/api/v1beta1"
13+
"github.com/replicatedhq/embedded-cluster/pkg/customization"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
"gopkg.in/yaml.v3"
17+
k8syaml "sigs.k8s.io/yaml"
18+
)
19+
20+
//go:embed testdata/*
21+
var testData embed.FS
22+
23+
func Test_patchK0sConfig(t *testing.T) {
24+
type test struct {
25+
Name string
26+
Original string `yaml:"original"`
27+
Override string `yaml:"override"`
28+
Expected string `yaml:"expected"`
29+
}
30+
entries, err := testData.ReadDir("testdata/patch-k0s-config")
31+
assert.NoError(t, err)
32+
var tests []test
33+
for _, entry := range entries {
34+
if strings.HasPrefix(entry.Name(), "skip.") {
35+
continue
36+
}
37+
fpath := path.Join("testdata", "patch-k0s-config", entry.Name())
38+
data, err := testData.ReadFile(fpath)
39+
assert.NoError(t, err)
40+
var onetest test
41+
err = yaml.Unmarshal(data, &onetest)
42+
assert.NoError(t, err)
43+
onetest.Name = fpath
44+
tests = append(tests, onetest)
45+
}
46+
for _, tt := range tests {
47+
t.Run(tt.Name, func(t *testing.T) {
48+
req := require.New(t)
49+
50+
originalFile, err := os.CreateTemp("", "k0s-original-*.yaml")
51+
req.NoError(err, "unable to create temp file")
52+
defer func() {
53+
originalFile.Close()
54+
os.Remove(originalFile.Name())
55+
}()
56+
err = os.WriteFile(originalFile.Name(), []byte(tt.Original), 0644)
57+
req.NoError(err, "unable to write original config")
58+
59+
var patch string
60+
if tt.Override != "" {
61+
var overrides embeddedclusterv1beta1.Config
62+
err = k8syaml.Unmarshal([]byte(tt.Override), &overrides)
63+
req.NoError(err, "unable to unmarshal override")
64+
patch = overrides.Spec.UnsupportedOverrides.K0s
65+
}
66+
67+
err = patchK0sConfig(originalFile.Name(), patch)
68+
req.NoError(err, "unable to patch config")
69+
70+
var original dig.Mapping
71+
err = yaml.NewDecoder(originalFile).Decode(&original)
72+
req.NoError(err, "unable to decode original file")
73+
74+
var expected dig.Mapping
75+
err = yaml.Unmarshal([]byte(tt.Expected), &expected)
76+
req.NoError(err, "unable to unmarshal expected file")
77+
78+
assert.Equal(t, expected, original)
79+
customization.DefaultProvider = &customization.AdminConsole{}
80+
})
81+
}
82+
}
83+
84+
func TestJoinCommandResponseOverrides(t *testing.T) {
85+
type test struct {
86+
Name string
87+
EmbeddedOverrides string `yaml:"embeddedOverrides"`
88+
EndUserOverrides string `yaml:"endUserOverrides"`
89+
ExpectedEmbeddedOverrides string `yaml:"expectedEmbeddedOverrides"`
90+
ExpectedUserOverrides string `yaml:"expectedUserOverrides"`
91+
}
92+
entries, err := testData.ReadDir("testdata/join-command-response")
93+
assert.NoError(t, err)
94+
var tests []test
95+
for _, entry := range entries {
96+
if strings.HasPrefix(entry.Name(), "skip.") {
97+
continue
98+
}
99+
fpath := path.Join("testdata", "join-command-response", entry.Name())
100+
data, err := testData.ReadFile(fpath)
101+
assert.NoError(t, err)
102+
var onetest test
103+
err = yaml.Unmarshal(data, &onetest)
104+
assert.NoError(t, err)
105+
onetest.Name = fpath
106+
tests = append(tests, onetest)
107+
}
108+
for _, tt := range tests {
109+
t.Run(tt.Name, func(t *testing.T) {
110+
req := require.New(t)
111+
join := JoinCommandResponse{
112+
K0sUnsupportedOverrides: tt.EmbeddedOverrides,
113+
EndUserK0sConfigOverrides: tt.EndUserOverrides,
114+
}
115+
116+
embedded, err := join.EmbeddedOverrides()
117+
req.NoError(err, "unable to patch config")
118+
expectedEmbedded := dig.Mapping{}
119+
err = yaml.Unmarshal([]byte(tt.ExpectedEmbeddedOverrides), &expectedEmbedded)
120+
req.NoError(err, "unable to unmarshal expected file")
121+
embeddedStr := fmt.Sprintf("%+v", embedded)
122+
expectedEmbeddedStr := fmt.Sprintf("%+v", expectedEmbedded)
123+
assert.Equal(t, expectedEmbeddedStr, embeddedStr)
124+
125+
user, err := join.EndUserOverrides()
126+
req.NoError(err, "unable to patch config")
127+
expectedUser := dig.Mapping{}
128+
err = yaml.Unmarshal([]byte(tt.ExpectedUserOverrides), &expectedUser)
129+
req.NoError(err, "unable to unmarshal expected file")
130+
userStr := fmt.Sprintf("%+v", user)
131+
expectedUserStr := fmt.Sprintf("%+v", expectedUser)
132+
assert.Equal(t, expectedUserStr, userStr)
133+
})
134+
}
135+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
endUserOverrides: ""
2+
embeddedOverrides: ""
3+
expectedUserOverrides: ""
4+
expectedEmbeddedOverrides: ""

0 commit comments

Comments
 (0)