Skip to content

Commit e416aba

Browse files
committed
Add possibility to copy files from guest to host
This makes the configuration into a one-liner (variable/path). The start command can do the "mkdir" and "cat" automatically. Signed-off-by: Anders F Björklund <[email protected]>
1 parent 0f37806 commit e416aba

File tree

8 files changed

+108
-10
lines changed

8 files changed

+108
-10
lines changed

examples/default.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,13 @@ networks:
336336
# hostPortRange: [1, 65535]
337337
# # Any port still not matched by a rule will not be forwarded (ignored)
338338

339+
# Copy files from the guest to the host. Copied after provisioning scripts have been completed.
340+
# copyToHost:
341+
# - guest: "/etc/myconfig.cfg"
342+
# host: "{{.Dir}}/copied-from-guest/myconfig"
343+
# # "guest" can include these template variables: {{.Home}}, {{.UID}}, and {{.User}}.
344+
# # "host" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, and {{.User}}.
345+
339346
# Message. Information to be shown to the user, given as a Go template for the instance.
340347
# The same template variables as for listing instances can be used, for example {{.Dir}}.
341348
# You can view the complete list of variables using `limactl list --list-fields` command.

examples/k3s.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
# It can be accessed from the host by exporting the kubeconfig file;
66
# the ports are already forwarded automatically by lima:
77
#
8-
# $ export KUBECONFIG=$PWD/kubeconfig.yaml
9-
# $ limactl shell k3s sudo cat /etc/rancher/k3s/k3s.yaml >$KUBECONFIG
8+
# $ export KUBECONFIG=$(limactl list k3s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')
109
# $ kubectl get no
1110
# NAME STATUS ROLES AGE VERSION
1211
# lima-k3s Ready control-plane,master 69s v1.21.1+k3s1
@@ -54,11 +53,12 @@ probes:
5453
The k3s kubeconfig file has not yet been created.
5554
Run "limactl shell k3s sudo journalctl -u k3s" to check the log.
5655
If that is still empty, check the bottom of the log at "/var/log/cloud-init-output.log".
56+
copyToHost:
57+
- guest: "/etc/rancher/k3s/k3s.yaml"
58+
host: "{{.Dir}}/copied-from-guest/kubeconfig.yaml"
5759
message: |
5860
To run `kubectl` on the host (assumes kubectl is installed), run the following commands:
5961
------
60-
mkdir -p "{{.Dir}}/conf"
61-
export KUBECONFIG="{{.Dir}}/conf/kubeconfig.yaml"
62-
limactl shell {{.Name}} sudo cat /etc/rancher/k3s/k3s.yaml >$KUBECONFIG
62+
export KUBECONFIG="{{.Dir}}/copied-from-guest/kubeconfig.yaml"
6363
kubectl ...
6464
------

examples/k8s.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
# It can be accessed from the host by exporting the kubeconfig file;
66
# the ports are already forwarded automatically by lima:
77
#
8-
# $ export KUBECONFIG=$PWD/kubeconfig.yaml
9-
# $ limactl shell k8s sudo cat /etc/kubernetes/admin.conf >$KUBECONFIG
8+
# $ export KUBECONFIG=$(limactl list k8s --format 'unix://{{.Dir}}/copied-from-guest/kubeconfig.yaml')
109
# $ kubectl get no
1110
# NAME STATUS ROLES AGE VERSION
1211
# lima-k8s Ready control-plane,master 44s v1.22.3
@@ -155,11 +154,12 @@ probes:
155154
echo >&2 "kubernetes cluster is not up and running yet"
156155
exit 1
157156
fi
157+
copyToHost:
158+
- guest: "/etc/kubernetes/admin.conf"
159+
host: "{{.Dir}}/copied-from-guest/kubeconfig.yaml"
158160
message: |
159161
To run `kubectl` on the host (assumes kubectl is installed), run the following commands:
160162
------
161-
mkdir -p "{{.Dir}}/conf"
162-
export KUBECONFIG="{{.Dir}}/conf/kubeconfig.yaml"
163-
limactl shell {{.Name}} sudo cat /etc/kubernetes/admin.conf >$KUBECONFIG
163+
export KUBECONFIG="{{.Dir}}/copied-from-guest/kubeconfig.yaml"
164164
kubectl ...
165165
------

pkg/hostagent/hostagent.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,12 @@ func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error {
356356
if err := a.waitForRequirements(ctx, "final", a.finalRequirements()); err != nil {
357357
mErr = multierror.Append(mErr, err)
358358
}
359+
// Copy all config files _after_ the requirements are done
360+
for _, rule := range a.y.CopyToHost {
361+
if err := copyToHost(ctx, a.sshConfig, a.sshLocalPort, rule.HostFile, rule.GuestFile); err != nil {
362+
mErr = multierror.Append(mErr, err)
363+
}
364+
}
359365
return mErr
360366
}
361367

@@ -525,3 +531,30 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
525531
}
526532
return nil
527533
}
534+
535+
func copyToHost(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string) error {
536+
args := sshConfig.Args()
537+
args = append(args,
538+
"-p", strconv.Itoa(port),
539+
"127.0.0.1",
540+
"--",
541+
)
542+
args = append(args,
543+
"sudo",
544+
"cat",
545+
remote,
546+
)
547+
logrus.Infof("Copying config from %s to %s", remote, local)
548+
if err := os.MkdirAll(filepath.Dir(local), 0700); err != nil {
549+
return fmt.Errorf("can't create directory for local file %q: %w", local, err)
550+
}
551+
cmd := exec.CommandContext(ctx, sshConfig.Binary(), args...)
552+
out, err := cmd.Output()
553+
if err != nil {
554+
return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
555+
}
556+
if err := os.WriteFile(local, out, 0600); err != nil {
557+
return fmt.Errorf("can't write to local file %q: %w", local, err)
558+
}
559+
return nil
560+
}

pkg/limayaml/defaults.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,11 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
315315
// After defaults processing the singular HostPort and GuestPort values should not be used again.
316316
}
317317

318+
y.CopyToHost = append(append(o.CopyToHost, y.CopyToHost...), d.CopyToHost...)
319+
for i := range y.CopyToHost {
320+
FillCopyToHostDefaults(&y.CopyToHost[i], instDir)
321+
}
322+
318323
if y.HostResolver.Enabled == nil {
319324
y.HostResolver.Enabled = d.HostResolver.Enabled
320325
}
@@ -618,6 +623,23 @@ func FillPortForwardDefaults(rule *PortForward, instDir string) {
618623
}
619624
}
620625

626+
func FillCopyToHostDefaults(rule *CopyToHost, instDir string) {
627+
if rule.GuestFile != "" {
628+
if out, err := executeGuestTemplate(rule.GuestFile); err == nil {
629+
rule.GuestFile = out.String()
630+
} else {
631+
logrus.WithError(err).Warnf("Couldn't process guest %q as a template", rule.GuestFile)
632+
}
633+
}
634+
if rule.HostFile != "" {
635+
if out, err := executeHostTemplate(rule.HostFile, instDir); err == nil {
636+
rule.HostFile = out.String()
637+
} else {
638+
logrus.WithError(err).Warnf("Couldn't process host %q as a template", rule.HostFile)
639+
}
640+
}
641+
}
642+
621643
func NewArch(arch string) Arch {
622644
switch arch {
623645
case "amd64":

pkg/limayaml/defaults_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ func TestFillDefault(t *testing.T) {
135135
HostSocket: "{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}}",
136136
},
137137
},
138+
CopyToHost: []CopyToHost{
139+
{
140+
GuestFile: "{{.Home}} | {{.UID}} | {{.User}}",
141+
HostFile: "{{.Home}} | {{.Dir}} | {{.Name}} | {{.UID}} | {{.User}}",
142+
},
143+
},
138144
Env: map[string]string{
139145
"ONE": "Eins",
140146
},
@@ -183,6 +189,10 @@ func TestFillDefault(t *testing.T) {
183189
defaultPortForward,
184190
defaultPortForward,
185191
}
192+
expect.CopyToHost = []CopyToHost{
193+
{},
194+
}
195+
186196
// Setting GuestPort and HostPort for DeepEqual(), but they are not supposed to be used
187197
// after FillDefault() has been called and the ...PortRange fields have been set.
188198
expect.PortForwards[1].GuestPort = 80
@@ -197,6 +207,9 @@ func TestFillDefault(t *testing.T) {
197207
expect.PortForwards[3].GuestSocket = fmt.Sprintf("%s | %s | %s", guestHome, user.Uid, user.Username)
198208
expect.PortForwards[3].HostSocket = fmt.Sprintf("%s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username)
199209

210+
expect.CopyToHost[0].GuestFile = fmt.Sprintf("%s | %s | %s", guestHome, user.Uid, user.Username)
211+
expect.CopyToHost[0].HostFile = fmt.Sprintf("%s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username)
212+
200213
expect.Env = y.Env
201214

202215
expect.CACertificates = CACertificates{
@@ -298,6 +311,7 @@ func TestFillDefault(t *testing.T) {
298311
HostPortRange: [2]int{80, 80},
299312
Proto: TCP,
300313
}},
314+
CopyToHost: []CopyToHost{{}},
301315
Env: map[string]string{
302316
"ONE": "one",
303317
"TWO": "two",
@@ -346,6 +360,7 @@ func TestFillDefault(t *testing.T) {
346360
expect.Provision = append(y.Provision, d.Provision...)
347361
expect.Probes = append(y.Probes, d.Probes...)
348362
expect.PortForwards = append(y.PortForwards, d.PortForwards...)
363+
expect.CopyToHost = append(y.CopyToHost, d.CopyToHost...)
349364
expect.Containerd.Archives = append(y.Containerd.Archives, d.Containerd.Archives...)
350365
expect.AdditionalDisks = append(y.AdditionalDisks, d.AdditionalDisks...)
351366

@@ -465,6 +480,7 @@ func TestFillDefault(t *testing.T) {
465480
HostPortRange: [2]int{8080, 8080},
466481
Proto: TCP,
467482
}},
483+
CopyToHost: []CopyToHost{{}},
468484
Env: map[string]string{
469485
"TWO": "deux",
470486
"THREE": "trois",
@@ -481,6 +497,7 @@ func TestFillDefault(t *testing.T) {
481497
expect.Provision = append(append(o.Provision, y.Provision...), d.Provision...)
482498
expect.Probes = append(append(o.Probes, y.Probes...), d.Probes...)
483499
expect.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...)
500+
expect.CopyToHost = append(append(o.CopyToHost, y.CopyToHost...), d.CopyToHost...)
484501
expect.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), d.Containerd.Archives...)
485502
expect.AdditionalDisks = append(append(o.AdditionalDisks, y.AdditionalDisks...), d.AdditionalDisks...)
486503

pkg/limayaml/limayaml.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type LimaYAML struct {
2424
Containerd Containerd `yaml:"containerd,omitempty" json:"containerd,omitempty"`
2525
Probes []Probe `yaml:"probes,omitempty" json:"probes,omitempty"`
2626
PortForwards []PortForward `yaml:"portForwards,omitempty" json:"portForwards,omitempty"`
27+
CopyToHost []CopyToHost `yaml:"copyToHost,omitempty" json:"copyToHost,omitempty"`
2728
Message string `yaml:"message,omitempty" json:"message,omitempty"`
2829
Networks []Network `yaml:"networks,omitempty" json:"networks,omitempty"`
2930
// `network` was deprecated in Lima v0.7.0, removed in Lima v0.14.0. Use `networks` instead.
@@ -179,6 +180,11 @@ type PortForward struct {
179180
Ignore bool `yaml:"ignore,omitempty" json:"ignore,omitempty"`
180181
}
181182

183+
type CopyToHost struct {
184+
GuestFile string `yaml:"guest,omitempty" json:"guest,omitempty"`
185+
HostFile string `yaml:"host,omitempty" json:"host,omitempty"`
186+
}
187+
182188
type Network struct {
183189
// `Lima`, `Socket`, and `VNL` are mutually exclusive; exactly one is required
184190
Lima string `yaml:"lima,omitempty" json:"lima,omitempty"`

pkg/limayaml/validate.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,19 @@ func Validate(y LimaYAML, warn bool) error {
249249
// Not validating that the various GuestPortRanges and HostPortRanges are not overlapping. Rules will be
250250
// processed sequentially and the first matching rule for a guest port determines forwarding behavior.
251251
}
252+
for i, rule := range y.CopyToHost {
253+
field := fmt.Sprintf("CopyToHost[%d]", i)
254+
if rule.GuestFile != "" {
255+
if !path.IsAbs(rule.GuestFile) {
256+
return fmt.Errorf("field `%s.guest` must be an absolute path", field)
257+
}
258+
}
259+
if rule.HostFile != "" {
260+
if !filepath.IsAbs(rule.HostFile) {
261+
return fmt.Errorf("field `%s.host` must be an absolute path, but is %q", field, rule.HostFile)
262+
}
263+
}
264+
}
252265

253266
if y.HostResolver.Enabled != nil && *y.HostResolver.Enabled && len(y.DNS) > 0 {
254267
return fmt.Errorf("field `dns` must be empty when field `HostResolver.Enabled` is true")

0 commit comments

Comments
 (0)