Skip to content

Commit 8a619c2

Browse files
authored
Merge pull request #978 from k0sproject/remember-config-origin
Resolve relative paths relative to config file location
2 parents d2bc2fe + ad10267 commit 8a619c2

10 files changed

Lines changed: 336 additions & 45 deletions

File tree

cmd/flags.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,22 @@ func initConfig(ctx *cli.Context) error {
226226

227227
log.Debugf("parsing configuration from %s", f)
228228

229-
if err := manifestReader.ParseBytes(subst); err != nil {
229+
origin := cfgName
230+
if cfgName == "-" {
231+
cwd, err := os.Getwd()
232+
if err != nil {
233+
return fmt.Errorf("failed to determine working directory: %w", err)
234+
}
235+
origin = cwd
236+
} else if named, ok := cfgFile.(interface{ Name() string }); ok {
237+
if n := named.Name(); n != "" {
238+
origin = n
239+
}
240+
} else if abs, err := filepath.Abs(origin); err == nil {
241+
origin = abs
242+
}
243+
244+
if err := manifestReader.ParseBytesWithOrigin(subst, origin); err != nil {
230245
return fmt.Errorf("failed to parse config: %w", err)
231246
}
232247

@@ -280,6 +295,10 @@ func readConfig(ctx *cli.Context) (*v1beta1.Cluster, error) {
280295
if err := ctlConfigs[0].Unmarshal(cfg); err != nil {
281296
return nil, fmt.Errorf("failed to unmarshal cluster config: %w", err)
282297
}
298+
cfg.Origin = ctlConfigs[0].Origin
299+
if err := cfg.Resolve(configBaseDir(cfg.Origin)); err != nil {
300+
return nil, fmt.Errorf("failed to resolve upload file paths: %w", err)
301+
}
283302
if k0sConfigs, err := mr.GetResources("k0s.k0sproject.io/v1beta1", "ClusterConfig"); err == nil && len(k0sConfigs) > 0 {
284303
if cfg.Spec.K0s.Config == nil {
285304
cfg.Spec.K0s.Config = make(dig.Mapping)
@@ -318,6 +337,20 @@ func readConfig(ctx *cli.Context) (*v1beta1.Cluster, error) {
318337
return cfg, nil
319338
}
320339

340+
func configBaseDir(origin string) string {
341+
if origin == "" {
342+
return ""
343+
}
344+
cleaned := origin
345+
if abs, err := filepath.Abs(origin); err == nil {
346+
cleaned = abs
347+
}
348+
if info, err := os.Stat(cleaned); err == nil && info.IsDir() {
349+
return filepath.ToSlash(cleaned)
350+
}
351+
return filepath.ToSlash(filepath.Dir(cleaned))
352+
}
353+
321354
func initManager(ctx *cli.Context) error {
322355
cfg, err := readConfig(ctx)
323356
if err != 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Cluster struct {
2929
Kind string `yaml:"kind"`
3030
Metadata *ClusterMetadata `yaml:"metadata"`
3131
Spec *cluster.Spec `yaml:"spec"`
32+
Origin string `yaml:"-"`
3233
}
3334

3435
// UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml
@@ -110,3 +111,11 @@ func (c *Cluster) StorageType() string {
110111
// default to etcd otherwise
111112
return "etcd"
112113
}
114+
115+
// Resolve prepares cluster-level data after unmarshalling by cascading down to the spec.
116+
func (c *Cluster) Resolve(baseDir string) error {
117+
if c.Spec == nil {
118+
return nil
119+
}
120+
return c.Spec.Resolve(baseDir)
121+
}

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

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package cluster
22

33
import (
4-
"context"
5-
"fmt"
6-
"net/url"
7-
gos "os"
8-
gopath "path"
9-
"slices"
4+
"context"
5+
"fmt"
6+
"net/url"
7+
gos "os"
8+
gopath "path"
9+
"slices"
1010
"strings"
1111
"time"
1212

@@ -132,6 +132,16 @@ func (h *Host) Validate() error {
132132
)
133133
}
134134

135+
// ResolveUploadFiles resolves host file sources relative to baseDir.
136+
func (h *Host) ResolveUploadFiles(baseDir string) error {
137+
for _, f := range h.Files {
138+
if err := f.ResolveRelativeTo(baseDir); err != nil {
139+
return err
140+
}
141+
}
142+
return nil
143+
}
144+
135145
type configurer interface {
136146
Kind() string
137147
CheckPrivilege(os.Host) error
@@ -203,6 +213,11 @@ type HostMetadata struct {
203213
DryRunFakeLeader bool
204214
}
205215

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)
219+
}
220+
206221
// UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml
207222
func (h *Host) UnmarshalYAML(unmarshal func(interface{}) error) error {
208223
type host Host
@@ -686,26 +701,26 @@ func (h *Host) FlagsChanged() bool {
686701

687702
// HasHooks returns true when the host has hooks defined for the action and stage.
688703
func (h *Host) HasHooks(action, stage string) bool {
689-
return len(h.Hooks.ForActionAndStage(action, stage)) > 0
704+
return len(h.Hooks.ForActionAndStage(action, stage)) > 0
690705
}
691706

692707
// RunHooks runs the hooks for the given action and stage (such as "apply", "before" would run the "before apply" hooks).
693708
// It respects context cancellation between hook executions.
694709
func (h *Host) RunHooks(ctx context.Context, action, stage string) error {
695-
commands := h.Hooks.ForActionAndStage(action, stage)
696-
if len(commands) == 0 {
697-
return nil
698-
}
699-
for _, cmd := range commands {
700-
// Abort early if the context has been canceled.
701-
if err := ctx.Err(); err != nil {
702-
return err
703-
}
704-
705-
log.Infof("%s: running %s %s hook: %q", h, stage, action, cmd)
706-
if err := h.Exec(cmd); err != nil {
707-
return fmt.Errorf("failed to execute hook %q for action %q stage %q on host %s: %w", cmd, action, stage, h.Address(), err)
708-
}
709-
}
710-
return nil
710+
commands := h.Hooks.ForActionAndStage(action, stage)
711+
if len(commands) == 0 {
712+
return nil
713+
}
714+
for _, cmd := range commands {
715+
// Abort early if the context has been canceled.
716+
if err := ctx.Err(); err != nil {
717+
return err
718+
}
719+
720+
log.Infof("%s: running %s %s hook: %q", h, stage, action, cmd)
721+
if err := h.Exec(cmd); err != nil {
722+
return fmt.Errorf("failed to execute hook %q for action %q stage %q on host %s: %w", cmd, action, stage, h.Address(), err)
723+
}
724+
}
725+
return nil
711726
}

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

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

41+
// Resolve runs Host.Resolve for each host.
42+
func (hosts Hosts) Resolve(baseDir string) error {
43+
for _, h := range hosts {
44+
if err := h.Resolve(baseDir); err != nil {
45+
return err
46+
}
47+
}
48+
return nil
49+
}
50+
4151
// First returns the first host
4252
func (hosts Hosts) First() *Host {
4353
if len(hosts) == 0 {

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,21 @@ func (s *Spec) Validate() error {
9696
)
9797
}
9898

99+
// ResolveUploadFilePaths resolves all host file sources relative to baseDir.
100+
func (s *Spec) ResolveUploadFilePaths(baseDir string) error {
101+
for _, h := range s.Hosts {
102+
if err := h.ResolveUploadFiles(baseDir); err != nil {
103+
return err
104+
}
105+
}
106+
return nil
107+
}
108+
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+
99114
type k0sCPLBConfig struct {
100115
Spec struct {
101116
Network struct {
@@ -187,6 +202,8 @@ func (s *Spec) NodeInternalKubeAPIURL(h *Host) string {
187202
return fmt.Sprintf("https://%s:%d", formatIPV6(addr), s.APIPort())
188203
}
189204

205+
// Resolve prepares spec-scoped resources after unmarshalling.
206+
// Currently cascades resolution into hosts using the given origin.
190207
func formatIPV6(address string) string {
191208
if strings.Contains(address, ":") {
192209
return fmt.Sprintf("[%s]", address)

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

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path"
7+
"path/filepath"
78
"strconv"
89
"strings"
910

@@ -88,6 +89,7 @@ func permToString(val interface{}) (string, error) {
8889
}
8990

9091
// UnmarshalYAML sets in some sane defaults when unmarshaling the data from yaml
92+
9193
func (u *UploadFile) UnmarshalYAML(unmarshal func(interface{}) error) error {
9294
type uploadFile UploadFile
9395
yu := (*uploadFile)(u)
@@ -108,7 +110,7 @@ func (u *UploadFile) UnmarshalYAML(unmarshal func(interface{}) error) error {
108110
}
109111
u.DirPermString = dp
110112

111-
return u.resolve()
113+
return nil
112114
}
113115

114116
// String returns the file bundle name or if it is empty, the source.
@@ -129,8 +131,8 @@ func isGlob(s string) bool {
129131
return strings.ContainsAny(s, "*%?[]{}")
130132
}
131133

132-
// sets the destination and resolves any globs/local paths into u.Sources
133-
func (u *UploadFile) resolve() error {
134+
// ResolveRelativeTo sets the destination and resolves globs/local paths relative to baseDir.
135+
func (u *UploadFile) ResolveRelativeTo(baseDir string) error {
134136
if u.IsURL() {
135137
if u.DestinationFile == "" {
136138
if u.DestinationDir != "" {
@@ -146,27 +148,42 @@ func (u *UploadFile) resolve() error {
146148
return nil
147149
}
148150

151+
u.Base = ""
152+
u.Sources = nil
153+
154+
src := filepath.ToSlash(u.Source)
155+
if src == "" {
156+
return fmt.Errorf("failed to resolve local path for %s: empty source", u)
157+
}
158+
if !path.IsAbs(src) {
159+
if baseDir != "" {
160+
src = path.Join(baseDir, src)
161+
}
162+
}
163+
src = path.Clean(src)
164+
149165
if isGlob(u.Source) {
150-
return u.glob(u.Source)
166+
return u.glob(src)
151167
}
152168

153-
stat, err := os.Stat(u.Source)
169+
fsPath := filepath.FromSlash(src)
170+
stat, err := os.Stat(fsPath)
154171
if err != nil {
155172
return fmt.Errorf("failed to stat local path for %s: %w", u, err)
156173
}
157174

158175
if stat.IsDir() {
159-
log.Tracef("source %s is a directory, assuming %s/**/*", u.Source, u.Source)
160-
return u.glob(path.Join(u.Source, "**/*"))
176+
log.Tracef("source %s is a directory, assuming %s/**/*", src, src)
177+
return u.glob(path.Join(src, "**/*"))
161178
}
162179

163180
perm := u.PermString
164181
if perm == "" {
165182
perm = fmt.Sprintf("%o", stat.Mode())
166183
}
167-
u.Base = path.Dir(u.Source)
184+
u.Base = path.Dir(src)
168185
u.Sources = []*LocalFile{
169-
{Path: path.Base(u.Source), PermMode: perm},
186+
{Path: path.Base(src), PermMode: perm},
170187
}
171188

172189
return nil
@@ -176,7 +193,7 @@ func (u *UploadFile) resolve() error {
176193
func (u *UploadFile) glob(src string) error {
177194
base, pattern := doublestar.SplitPattern(src)
178195
u.Base = base
179-
fsys := os.DirFS(base)
196+
fsys := os.DirFS(filepath.FromSlash(base))
180197
sources, err := doublestar.Glob(fsys, pattern)
181198
if err != nil {
182199
return err
@@ -185,7 +202,7 @@ func (u *UploadFile) glob(src string) error {
185202
for _, s := range sources {
186203
abs := path.Join(base, s)
187204
log.Tracef("glob %s found: %s", abs, s)
188-
stat, err := os.Stat(abs)
205+
stat, err := os.Stat(filepath.FromSlash(abs))
189206
if err != nil {
190207
return fmt.Errorf("failed to stat file %s: %w", u, err)
191208
}

0 commit comments

Comments
 (0)