Skip to content

Commit ad10267

Browse files
committed
Improve config origin resolution cascade
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
1 parent d2a3932 commit ad10267

8 files changed

Lines changed: 148 additions & 49 deletions

File tree

cmd/flags.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,8 @@ func readConfig(ctx *cli.Context) (*v1beta1.Cluster, error) {
296296
return nil, fmt.Errorf("failed to unmarshal cluster config: %w", err)
297297
}
298298
cfg.Origin = ctlConfigs[0].Origin
299-
if cfg.Spec != nil {
300-
if err := cfg.Spec.ResolveUploadFilePaths(configBaseDir(cfg.Origin)); err != nil {
301-
return nil, fmt.Errorf("failed to resolve upload file paths: %w", err)
302-
}
299+
if err := cfg.Resolve(configBaseDir(cfg.Origin)); err != nil {
300+
return nil, fmt.Errorf("failed to resolve upload file paths: %w", err)
303301
}
304302
if k0sConfigs, err := mr.GetResources("k0s.k0sproject.io/v1beta1", "ClusterConfig"); err == nil && len(k0sConfigs) > 0 {
305303
if cfg.Spec.K0s.Config == nil {

cmd/flags_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"flag"
6+
"os"
7+
"path/filepath"
8+
"testing"
9+
10+
"github.com/k0sproject/k0sctl/pkg/manifest"
11+
"github.com/stretchr/testify/require"
12+
"github.com/urfave/cli/v2"
13+
)
14+
15+
func TestReadConfigSetsOrigin(t *testing.T) {
16+
origin := filepath.Join(t.TempDir(), "cluster.yaml")
17+
clusterYAML := `apiVersion: k0sctl.k0sproject.io/v1beta1
18+
kind: Cluster
19+
metadata:
20+
name: test
21+
spec:
22+
hosts:
23+
- role: controller
24+
ssh:
25+
address: 10.0.0.1
26+
`
27+
28+
mr := &manifest.Reader{}
29+
require.NoError(t, mr.ParseBytesWithOrigin([]byte(clusterYAML), origin))
30+
31+
app := cli.NewApp()
32+
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
33+
ctx := cli.NewContext(app, flagSet, nil)
34+
ctx.Context = context.WithValue(context.Background(), ctxConfigsKey{}, mr)
35+
36+
cfg, err := readConfig(ctx)
37+
require.NoError(t, err)
38+
require.Equal(t, origin, cfg.Origin)
39+
}
40+
41+
func TestReadConfigResolvesUploadFilesRelativeToOrigin(t *testing.T) {
42+
dir := t.TempDir()
43+
assetsDir := filepath.Join(dir, "assets", "bin")
44+
require.NoError(t, os.MkdirAll(assetsDir, 0o755))
45+
require.NoError(t, os.WriteFile(filepath.Join(assetsDir, "script.sh"), []byte("#!/bin/sh\n"), 0o755))
46+
47+
origin := filepath.Join(dir, "cluster.yaml")
48+
clusterYAML := `apiVersion: k0sctl.k0sproject.io/v1beta1
49+
kind: Cluster
50+
metadata:
51+
name: test
52+
spec:
53+
hosts:
54+
- role: controller
55+
ssh:
56+
address: 10.0.0.1
57+
files:
58+
- src: assets/bin/script.sh
59+
dstDir: /tmp
60+
`
61+
require.NoError(t, os.WriteFile(origin, []byte(clusterYAML), 0o644))
62+
63+
mr := &manifest.Reader{}
64+
require.NoError(t, mr.ParseBytesWithOrigin([]byte(clusterYAML), origin))
65+
66+
app := cli.NewApp()
67+
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
68+
ctx := cli.NewContext(app, flagSet, nil)
69+
ctx.Context = context.WithValue(context.Background(), ctxConfigsKey{}, mr)
70+
71+
cfg, err := readConfig(ctx)
72+
require.NoError(t, err)
73+
require.Equal(t, origin, cfg.Origin)
74+
require.Len(t, cfg.Spec.Hosts, 1)
75+
require.Len(t, cfg.Spec.Hosts[0].Files, 1)
76+
require.Equal(t, filepath.ToSlash(assetsDir), cfg.Spec.Hosts[0].Files[0].Base)
77+
require.Len(t, cfg.Spec.Hosts[0].Files[0].Sources, 1)
78+
require.Equal(t, "script.sh", cfg.Spec.Hosts[0].Files[0].Sources[0].Path)
79+
}

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,10 @@ func (c *Cluster) StorageType() string {
112112
return "etcd"
113113
}
114114

115-
// Resolve prepares cluster-scoped resources after unmarshalling.
116-
// Currently cascades resolution into spec using the given origin.
117-
func (c *Cluster) Resolve(origin string) error {
115+
// Resolve prepares cluster-level data after unmarshalling by cascading down to the spec.
116+
func (c *Cluster) Resolve(baseDir string) error {
118117
if c.Spec == nil {
119118
return nil
120119
}
121-
return c.Spec.Resolve(origin)
120+
return c.Spec.Resolve(baseDir)
122121
}

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/host.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,9 @@ type HostMetadata struct {
213213
DryRunFakeLeader bool
214214
}
215215

216-
// Resolve prepares host-scoped resources after unmarshalling.
217-
// Currently cascades resolution into file uploads using the given origin.
218-
func (h *Host) Resolve(origin string) error {
219-
for _, f := range h.Files {
220-
if err := f.Resolve(origin); err != nil {
221-
return fmt.Errorf("failed to resolve upload file %s: %w", f, err)
222-
}
223-
}
224-
return nil
216+
// Resolve prepares host-scoped data after unmarshalling by resolving upload files.
217+
func (h *Host) Resolve(baseDir string) error {
218+
return h.ResolveUploadFiles(baseDir)
225219
}
226220

227221
// UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/hosts.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ func (hosts Hosts) Validate() error {
3838
return nil
3939
}
4040

41-
// Resolve runs Host.Resolve(origin) for each host in the collection.
42-
func (hosts Hosts) Resolve(origin string) error {
41+
// Resolve runs Host.Resolve for each host.
42+
func (hosts Hosts) Resolve(baseDir string) error {
4343
for _, h := range hosts {
44-
if err := h.Resolve(origin); err != nil {
44+
if err := h.Resolve(baseDir); err != nil {
4545
return err
4646
}
4747
}

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/spec.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ func (s *Spec) ResolveUploadFilePaths(baseDir string) error {
106106
return nil
107107
}
108108

109+
// Resolve prepares spec-level data after unmarshalling by cascading to hosts.
110+
func (s *Spec) Resolve(baseDir string) error {
111+
return s.ResolveUploadFilePaths(baseDir)
112+
}
113+
109114
type k0sCPLBConfig struct {
110115
Spec struct {
111116
Network struct {
@@ -199,13 +204,6 @@ func (s *Spec) NodeInternalKubeAPIURL(h *Host) string {
199204

200205
// Resolve prepares spec-scoped resources after unmarshalling.
201206
// Currently cascades resolution into hosts using the given origin.
202-
func (s *Spec) Resolve(origin string) error {
203-
if err := s.Hosts.Resolve(origin); err != nil {
204-
return err
205-
}
206-
return nil
207-
}
208-
209207
func formatIPV6(address string) string {
210208
if strings.Contains(address, ":") {
211209
return fmt.Sprintf("[%s]", address)

pkg/apis/k0sctl.k0sproject.io/v1beta1/cluster/uploadfile_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,23 @@ dstDir: /tmp
135135
require.Len(t, u.Sources, 2)
136136
require.ElementsMatch(t, []string{"manifests/a.yaml", "manifests/b.yaml"}, []string{u.Sources[0].Path, u.Sources[1].Path})
137137
}
138+
139+
func TestUploadFileResolveRelativeURLSetsDestination(t *testing.T) {
140+
u := &UploadFile{Source: "https://example.com/assets/app.tar.gz", DestinationDir: "/opt"}
141+
require.NoError(t, u.ResolveRelativeTo(""))
142+
require.Equal(t, "/opt/app.tar.gz", u.DestinationFile)
143+
require.Equal(t, "", u.Base)
144+
require.Len(t, u.Sources, 0)
145+
}
146+
147+
func TestUploadFileResolveRelativeSingleFile(t *testing.T) {
148+
tmp := filepath.ToSlash(t.TempDir())
149+
filePath := filepath.Join(tmp, "a.txt")
150+
require.NoError(t, os.WriteFile(filePath, []byte("a"), 0o640))
151+
152+
u := &UploadFile{Source: "a.txt"}
153+
require.NoError(t, u.ResolveRelativeTo(tmp))
154+
require.Equal(t, tmp, u.Base)
155+
require.Len(t, u.Sources, 1)
156+
require.Equal(t, "a.txt", u.Sources[0].Path)
157+
}

pkg/manifest/reader.go

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,6 @@ type Reader struct {
7171
manifests []*ResourceDefinition
7272
}
7373

74-
type namedReader struct {
75-
reader io.Reader
76-
name string
77-
}
78-
79-
func (n *namedReader) Read(p []byte) (int, error) {
80-
return n.reader.Read(p)
81-
}
82-
83-
func (n *namedReader) Name() string {
84-
return n.name
85-
}
86-
8774
func name(r io.Reader) string {
8875
type named interface {
8976
Name() string
@@ -96,8 +83,32 @@ func name(r io.Reader) string {
9683
return "manifest"
9784
}
9885

86+
// ParseOption configures optional behavior for Parse.
87+
type ParseOption func(*parseOptions)
88+
89+
type parseOptions struct {
90+
origin string
91+
}
92+
93+
// WithOrigin overrides the origin name for resources parsed from the reader.
94+
func WithOrigin(origin string) ParseOption {
95+
return func(po *parseOptions) {
96+
po.origin = origin
97+
}
98+
}
99+
99100
// Parse parses Kubernetes resource definitions from the provided input stream. They are then available via the Resources() or GetResources(apiVersion, kind) methods.
100-
func (r *Reader) Parse(input io.Reader) error {
101+
func (r *Reader) Parse(input io.Reader, opts ...ParseOption) error {
102+
po := &parseOptions{}
103+
for _, opt := range opts {
104+
opt(po)
105+
}
106+
107+
origin := po.origin
108+
if origin == "" {
109+
origin = name(input)
110+
}
111+
101112
yamlReader := yamlutil.NewYAMLReader(bufio.NewReader(input))
102113

103114
for {
@@ -118,17 +129,17 @@ func (r *Reader) Parse(input io.Reader) error {
118129
if r.IgnoreErrors {
119130
continue
120131
}
121-
return fmt.Errorf("failed to decode resource %s: %w", name(input), err)
132+
return fmt.Errorf("failed to decode resource %s: %w", origin, err)
122133
}
123134

124135
if rd.APIVersion == "" || rd.Kind == "" {
125136
if r.IgnoreErrors {
126137
continue
127138
}
128-
return fmt.Errorf("missing apiVersion or kind in resource %s", name(input))
139+
return fmt.Errorf("missing apiVersion or kind in resource %s", origin)
129140
}
130141

131-
rd.Origin = name(input)
142+
rd.Origin = origin
132143
// Store the raw chunk
133144
rd.Raw = append([]byte{}, rawChunk...)
134145
r.manifests = append(r.manifests, rd)
@@ -138,18 +149,18 @@ func (r *Reader) Parse(input io.Reader) error {
138149
}
139150

140151
// ParseString parses Kubernetes resource definitions from the provided string.
141-
func (r *Reader) ParseString(input string) error {
142-
return r.Parse(strings.NewReader(input))
152+
func (r *Reader) ParseString(input string, opts ...ParseOption) error {
153+
return r.Parse(strings.NewReader(input), opts...)
143154
}
144155

145156
// ParseBytes parses Kubernetes resource definitions from the provided byte slice.
146-
func (r *Reader) ParseBytes(input []byte) error {
147-
return r.ParseBytesWithOrigin(input, "")
157+
func (r *Reader) ParseBytes(input []byte, opts ...ParseOption) error {
158+
return r.Parse(bytes.NewReader(input), opts...)
148159
}
149160

150161
// ParseBytesWithOrigin parses raw bytes and attributes them to the provided origin.
151162
func (r *Reader) ParseBytesWithOrigin(input []byte, origin string) error {
152-
return r.Parse(&namedReader{reader: bytes.NewReader(input), name: origin})
163+
return r.ParseBytes(input, WithOrigin(origin))
153164
}
154165

155166
// Resources returns all parsed Kubernetes resource definitions.

0 commit comments

Comments
 (0)