Skip to content

Commit 234dce5

Browse files
uwej711kke
andauthored
Create files on nodes from inlined content in config (#957)
* Upload file from inline content Signed-off-by: Uwe Jäger <[email protected]> Signed-off-by: Kimmo Lehto <[email protected]> * Install golangci-lint v2, fix lint error Signed-off-by: Uwe Jäger <[email protected]> Signed-off-by: Kimmo Lehto <[email protected]> * Rename "content" to "data" Signed-off-by: Kimmo Lehto <[email protected]> * Mention the new "data" field in README Signed-off-by: Kimmo Lehto <[email protected]> * Add "omitempty" to uploadfiles struct fields Signed-off-by: Kimmo Lehto <[email protected]> --------- Signed-off-by: Uwe Jäger <[email protected]> Signed-off-by: Kimmo Lehto <[email protected]> Co-authored-by: Kimmo Lehto <[email protected]>
1 parent 7921c89 commit 234dce5

File tree

7 files changed

+134
-12
lines changed

7 files changed

+134
-12
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ golint := $(shell go env GOPATH)/bin/golangci-lint
9898
endif
9999

100100
$(golint):
101-
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
101+
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
102102

103103
.PHONY: lint
104104
lint: $(golint)

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,19 @@ Example:
367367
perm: 0600
368368
```
369369

370+
Inline data example:
371+
372+
```yaml
373+
- name: motd
374+
data: |
375+
Powered by k0s
376+
dst: /etc/motd
377+
perm: 0644
378+
```
379+
370380
* `name`: name of the file "bundle", used only for logging purposes (optional)
371-
* `src`: File path, an URL or [Glob pattern](https://golang.org/pkg/path/filepath/#Match) to match files to be uploaded. URL sources will be directly downloaded using the target host. If the value is a URL, '%'-prefixed tokens can be used, see [tokens](#tokens). (required)
381+
* `src`: File path, an URL or [Glob pattern](https://golang.org/pkg/path/filepath/#Match) to match files to be uploaded. URL sources will be directly downloaded using the target host. If the value is a URL, '%'-prefixed tokens can be used, see [tokens](#tokens). (required when `data` is not set)
382+
* `data`: Inline file data to write to the destination. Use together with `dst` or `dst` + `dstDir`. (required when `src` is not set)
372383
* `dstDir`: Destination directory for the file(s). `k0sctl` will create full directory structure if it does not already exist on the host (default: user home)
373384
* `dst`: Destination filename for the file. Only usable for single file uploads (default: basename of file)
374385
* `perm`: File permission mode for uploaded file(s) (default: same as local)

phase/uploadfiles.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"path"
8+
"strconv"
89

910
"al.essio.dev/pkg/shellescape"
1011
"github.com/k0sproject/k0sctl/pkg/apis/k0sctl.k0sproject.io/v1beta1"
@@ -54,8 +55,10 @@ func (p *UploadFiles) uploadFiles(ctx context.Context, h *cluster.Host) error {
5455
var err error
5556
if f.IsURL() {
5657
err = p.uploadURL(h, f)
57-
} else {
58+
} else if len(f.Sources) > 0 {
5859
err = p.uploadFile(h, f)
60+
} else if f.HasData() {
61+
err = p.uploadData(h, f)
5962
}
6063
if err != nil {
6164
return err
@@ -168,6 +171,57 @@ func (p *UploadFiles) uploadFile(h *cluster.Host, f *cluster.UploadFile) error {
168171
return nil
169172
}
170173

174+
func (p *UploadFiles) uploadData(h *cluster.Host, f *cluster.UploadFile) error {
175+
log.Infof("%s: uploading inline data", h)
176+
dest := f.DestinationFile
177+
if dest == "" {
178+
if f.DestinationDir != "" {
179+
dest = path.Join(f.DestinationDir, f.Name)
180+
} else {
181+
dest = f.Name
182+
}
183+
}
184+
185+
owner := f.Owner()
186+
187+
if err := p.ensureDir(h, path.Dir(dest), f.DirPermString, owner); err != nil {
188+
return err
189+
}
190+
191+
err := p.Wet(h, fmt.Sprintf("upload inline data => %s", dest), func() error {
192+
fileMode, _ := strconv.ParseUint(f.PermString, 8, 32)
193+
remoteFile, err := h.SudoFsys().OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(fileMode))
194+
if err != nil {
195+
return err
196+
}
197+
198+
defer func() {
199+
if err := remoteFile.Close(); err != nil {
200+
log.Warnf("failed to close remote file %s: %v", dest, err)
201+
}
202+
}()
203+
204+
_, err = fmt.Fprint(remoteFile, f.Data)
205+
206+
return err
207+
})
208+
if err != nil {
209+
return err
210+
}
211+
212+
if owner != "" {
213+
err := p.Wet(h, fmt.Sprintf("set owner for %s to %s", dest, owner), func() error {
214+
log.Debugf("%s: setting owner %s for %s", h, owner, dest)
215+
return h.Execf(`chown %s %s`, shellescape.Quote(owner), shellescape.Quote(dest), exec.Sudo(h))
216+
})
217+
if err != nil {
218+
return err
219+
}
220+
}
221+
222+
return nil
223+
}
224+
171225
func (p *UploadFiles) uploadURL(h *cluster.Host, f *cluster.UploadFile) error {
172226
log.Infof("%s: downloading %s to host %s", h, f, f.DestinationFile)
173227
owner := f.Owner()

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ type LocalFile struct {
2020
// UploadFile describes a file to be uploaded for the host
2121
type UploadFile struct {
2222
Name string `yaml:"name,omitempty"`
23-
Source string `yaml:"src"`
24-
DestinationDir string `yaml:"dstDir"`
25-
DestinationFile string `yaml:"dst"`
26-
PermMode interface{} `yaml:"perm"`
27-
DirPermMode interface{} `yaml:"dirPerm"`
28-
User string `yaml:"user"`
29-
Group string `yaml:"group"`
23+
Source string `yaml:"src,omitempty"`
24+
Data string `yaml:"data,omitempty"`
25+
DestinationDir string `yaml:"dstDir,omitempty"`
26+
DestinationFile string `yaml:"dst,omitempty"`
27+
PermMode interface{} `yaml:"perm,omitempty"`
28+
DirPermMode interface{} `yaml:"dirPerm,omitempty"`
29+
User string `yaml:"user,omitempty"`
30+
Group string `yaml:"group,omitempty"`
3031
PermString string `yaml:"-"`
3132
DirPermString string `yaml:"-"`
3233
Sources []*LocalFile `yaml:"-"`
@@ -35,7 +36,9 @@ type UploadFile struct {
3536

3637
func (u UploadFile) Validate() error {
3738
return validation.ValidateStruct(&u,
38-
validation.Field(&u.Source, validation.Required),
39+
validation.Field(&u.Name, validation.Required.When(u.HasData() && u.DestinationFile == "").Error("name or dst required for data")),
40+
validation.Field(&u.Source, validation.Required.When(!u.HasData()).Error("src or data required")),
41+
validation.Field(&u.Data, validation.Required.When(u.Source == "").Error("src or data required")),
3942
validation.Field(&u.DestinationFile, validation.Required.When(u.DestinationDir == "").Error("dst or dstdir required")),
4043
validation.Field(&u.DestinationDir, validation.Required.When(u.DestinationFile == "").Error("dst or dstdir required")),
4144
)
@@ -139,6 +142,10 @@ func (u *UploadFile) resolve() error {
139142
return nil
140143
}
141144

145+
if u.HasData() {
146+
return nil
147+
}
148+
142149
if isGlob(u.Source) {
143150
return u.glob(u.Source)
144151
}
@@ -211,3 +218,7 @@ func (u *UploadFile) glob(src string) error {
211218
func (u *UploadFile) IsURL() bool {
212219
return strings.Contains(u.Source, "://")
213220
}
221+
222+
func (u *UploadFile) HasData() bool {
223+
return strings.TrimSpace(u.Data) != ""
224+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,33 @@ perm: 0
6363

6464
require.Error(t, yaml.Unmarshal(yml, &u))
6565
}
66+
67+
func TestUploadFileValidateRequiresDestinationFileForData(t *testing.T) {
68+
u := UploadFile{Data: "hello", DestinationDir: "/tmp"}
69+
70+
err := u.Validate()
71+
require.Error(t, err)
72+
require.Contains(t, err.Error(), "name or dst required for data")
73+
}
74+
75+
func TestUploadFileValidateDataWithDestinationFile(t *testing.T) {
76+
u := UploadFile{Data: "hello", DestinationFile: "/tmp/inline.txt"}
77+
78+
require.NoError(t, u.Validate())
79+
}
80+
81+
func TestUploadFileValidateRequiresSourceOrData(t *testing.T) {
82+
u := UploadFile{Data: " "}
83+
84+
err := u.Validate()
85+
require.Error(t, err)
86+
require.Contains(t, err.Error(), "src or data required")
87+
}
88+
89+
func TestUploadFileValidateRequiresDestinationFileOrName(t *testing.T) {
90+
u := UploadFile{Data: "hello", DestinationDir: "/tmp/"}
91+
92+
err := u.Validate()
93+
require.Error(t, err)
94+
require.Contains(t, err.Error(), "name or dst required for data")
95+
}

smoke-test/k0sctl-files.yaml.tpl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ spec:
1414
dst: /root/singlefile/renamed.txt
1515
user: test
1616
group: test
17+
- name: inline-data
18+
dst: /root/content/hello.sh
19+
user: test
20+
group: test
21+
perm: 0755
22+
data: |-
23+
#!/bin/sh
24+
echo hello
1725
- name: dest_dir
1826
src: ./upload/toplevel.txt
1927
dstDir: /root/destdir

smoke-test/smoke-files.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ remoteCommand root@manager0 stat -c '%U:%G' /root/singlefile/renamed.txt | grep
6060
printf %s "[stat]"
6161
echo "OK"
6262

63+
printf %s " - File from inline data .. "
64+
remoteFileExist root@manager0 /root/content/hello.sh
65+
printf %s "[exist]"
66+
remoteCommand root@manager0 stat -c '%U:%G' /root/content/hello.sh | grep -q test:test
67+
printf %s "[stat]"
68+
remoteCommand root@manager0 /root/content/hello.sh | grep -q hello
69+
printf %s "[run] "
70+
echo "OK"
71+
6372
printf %s " - Single file using destination dir .. "
6473
remoteFileExist root@manager0 /root/destdir/toplevel.txt
6574
echo "OK"
@@ -117,4 +126,3 @@ printf %s "[content] "
117126
echo "OK"
118127

119128
echo "* Done"
120-

0 commit comments

Comments
 (0)