Skip to content

Commit 6ccce63

Browse files
committed
Add possibility to copy ConfigFile to host
This makes the configuration into a one-liner (variable/path). The start command can do the "mkdir" and "cat" automatically.
1 parent 25353ef commit 6ccce63

File tree

9 files changed

+135
-8
lines changed

9 files changed

+135
-8
lines changed

examples/default.yaml

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

339+
# Config files. Copied after provisioning scripts have been completed.
340+
# configFiles:
341+
# - guestFile: "/etc/myconfig.cfg"
342+
# hostFile: myconfig
343+
# # default: mode: system
344+
# # "guestFile" can include these template variables: {{.Home}}, {{.UID}}, and {{.User}}.
345+
# # "hostFile" can include {{.Home}}, {{.Dir}}, {{.Name}}, {{.UID}}, and {{.User}}.
346+
# # "mode" can be "system" (with root) or "user" (without root), similar to provision
347+
# # Put configs into "{{.Dir}}/conf" to avoid collision with Lima internal configs!
348+
339349
# Message. Information to be shown to the user, given as a Go template for the instance.
340350
# The same template variables as for listing instances can be used, for example {{.Dir}}.
341351
# You can view the complete list of variables using `limactl list --list-fields` command.

examples/k3s.yaml

Lines changed: 4 additions & 4 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}}/conf/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+
configFiles:
57+
- guestFile: "/etc/rancher/k3s/k3s.yaml"
58+
hostFile: "{{.Dir}}/conf/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"
6162
export KUBECONFIG="{{.Dir}}/conf/kubeconfig.yaml"
62-
limactl shell {{.Name}} sudo cat /etc/rancher/k3s/k3s.yaml >$KUBECONFIG
6363
kubectl ...
6464
------

examples/k8s.yaml

Lines changed: 4 additions & 4 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}}/conf/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+
configFiles:
158+
- guestFile: "/etc/kubernetes/admin.conf"
159+
hostFile: "{{.Dir}}/conf/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"
162163
export KUBECONFIG="{{.Dir}}/conf/kubeconfig.yaml"
163-
limactl shell {{.Name}} sudo cat /etc/kubernetes/admin.conf >$KUBECONFIG
164164
kubectl ...
165165
------

pkg/hostagent/hostagent.go

Lines changed: 37 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.ConfigFiles {
361+
if err := copyConfig(ctx, a.sshConfig, a.sshLocalPort, rule.HostFile, rule.GuestFile, rule.Mode); err != nil {
362+
mErr = multierror.Append(mErr, err)
363+
}
364+
}
359365
return mErr
360366
}
361367

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

pkg/limayaml/defaults.go

Lines changed: 28 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.ConfigFiles = append(append(o.ConfigFiles, y.ConfigFiles...), d.ConfigFiles...)
319+
for i := range y.ConfigFiles {
320+
FillConfigFileDefaults(&y.ConfigFiles[i], instDir)
321+
}
322+
318323
if y.HostResolver.Enabled == nil {
319324
y.HostResolver.Enabled = d.HostResolver.Enabled
320325
}
@@ -618,6 +623,29 @@ func FillPortForwardDefaults(rule *PortForward, instDir string) {
618623
}
619624
}
620625

626+
func FillConfigFileDefaults(rule *ConfigFile, instDir string) {
627+
if rule.Mode == "" {
628+
rule.Mode = ProvisionModeSystem
629+
}
630+
if rule.GuestFile != "" {
631+
if out, err := processGuest(rule.GuestFile); err == nil {
632+
rule.GuestFile = out.String()
633+
} else {
634+
logrus.WithError(err).Warnf("Couldn't process guestFile %q as a template", rule.GuestFile)
635+
}
636+
}
637+
if rule.HostFile != "" {
638+
if out, err := processHost(rule.HostFile, instDir); err == nil {
639+
rule.HostFile = out.String()
640+
} else {
641+
logrus.WithError(err).Warnf("Couldn't process hostFile %q as a template", rule.HostFile)
642+
}
643+
if !filepath.IsAbs(rule.HostFile) {
644+
rule.HostFile = filepath.Join(instDir, filenames.InstanceConfigDir, rule.HostFile)
645+
}
646+
}
647+
}
648+
621649
func NewArch(arch string) Arch {
622650
switch arch {
623651
case "amd64":

pkg/limayaml/defaults_test.go

Lines changed: 22 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+
ConfigFiles: []ConfigFile{
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.ConfigFiles = []ConfigFile{
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,10 @@ 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.ConfigFiles[0].Mode = ProvisionModeSystem
211+
expect.ConfigFiles[0].GuestFile = fmt.Sprintf("%s | %s | %s", guestHome, user.Uid, user.Username)
212+
expect.ConfigFiles[0].HostFile = fmt.Sprintf("%s | %s | %s | %s | %s", hostHome, instDir, instName, user.Uid, user.Username)
213+
200214
expect.Env = y.Env
201215

202216
expect.CACertificates = CACertificates{
@@ -298,6 +312,9 @@ func TestFillDefault(t *testing.T) {
298312
HostPortRange: [2]int{80, 80},
299313
Proto: TCP,
300314
}},
315+
ConfigFiles: []ConfigFile{{
316+
Mode: ProvisionModeUser,
317+
}},
301318
Env: map[string]string{
302319
"ONE": "one",
303320
"TWO": "two",
@@ -346,6 +363,7 @@ func TestFillDefault(t *testing.T) {
346363
expect.Provision = append(y.Provision, d.Provision...)
347364
expect.Probes = append(y.Probes, d.Probes...)
348365
expect.PortForwards = append(y.PortForwards, d.PortForwards...)
366+
expect.ConfigFiles = append(y.ConfigFiles, d.ConfigFiles...)
349367
expect.Containerd.Archives = append(y.Containerd.Archives, d.Containerd.Archives...)
350368
expect.AdditionalDisks = append(y.AdditionalDisks, d.AdditionalDisks...)
351369

@@ -465,6 +483,9 @@ func TestFillDefault(t *testing.T) {
465483
HostPortRange: [2]int{8080, 8080},
466484
Proto: TCP,
467485
}},
486+
ConfigFiles: []ConfigFile{{
487+
Mode: ProvisionModeUser,
488+
}},
468489
Env: map[string]string{
469490
"TWO": "deux",
470491
"THREE": "trois",
@@ -481,6 +502,7 @@ func TestFillDefault(t *testing.T) {
481502
expect.Provision = append(append(o.Provision, y.Provision...), d.Provision...)
482503
expect.Probes = append(append(o.Probes, y.Probes...), d.Probes...)
483504
expect.PortForwards = append(append(o.PortForwards, y.PortForwards...), d.PortForwards...)
505+
expect.ConfigFiles = append(append(o.ConfigFiles, y.ConfigFiles...), d.ConfigFiles...)
484506
expect.Containerd.Archives = append(append(o.Containerd.Archives, y.Containerd.Archives...), d.Containerd.Archives...)
485507
expect.AdditionalDisks = append(append(o.AdditionalDisks, y.AdditionalDisks...), d.AdditionalDisks...)
486508

pkg/limayaml/limayaml.go

Lines changed: 7 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+
ConfigFiles []ConfigFile `yaml:"configFiles,omitempty" json:"configFiles,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,12 @@ type PortForward struct {
179180
Ignore bool `yaml:"ignore,omitempty" json:"ignore,omitempty"`
180181
}
181182

183+
type ConfigFile struct {
184+
Mode ProvisionMode `yaml:"mode" json:"mode"` // default: "system"
185+
GuestFile string `yaml:"guestFile,omitempty" json:"guestFile,omitempty"`
186+
HostFile string `yaml:"hostFile,omitempty" json:"hostFile,omitempty"`
187+
}
188+
182189
type Network struct {
183190
// `Lima`, `Socket`, and `VNL` are mutually exclusive; exactly one is required
184191
Lima string `yaml:"lima,omitempty" json:"lima,omitempty"`

pkg/limayaml/validate.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,26 @@ 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.ConfigFiles {
253+
field := fmt.Sprintf("ConfigFile[%d]", i)
254+
switch rule.Mode {
255+
case ProvisionModeSystem, ProvisionModeUser:
256+
default:
257+
return fmt.Errorf("field `%s.mode` must be either %q or %q",
258+
field, ProvisionModeSystem, ProvisionModeUser)
259+
}
260+
if rule.GuestFile != "" {
261+
if !path.IsAbs(rule.GuestFile) {
262+
return fmt.Errorf("field `%s.guestFile` must be an absolute path", field)
263+
}
264+
}
265+
if rule.HostFile != "" {
266+
if !filepath.IsAbs(rule.HostFile) {
267+
// should be unreachable because FillDefault() will prepend the instance directory to relative names
268+
return fmt.Errorf("field `%s.hostFile` must be an absolute path, but is %q", field, rule.HostFile)
269+
}
270+
}
271+
}
252272

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

pkg/store/filenames/filenames.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const (
4747

4848
// InstanceSocketDir is the default location for forwarded sockets with a relative path in HostSocket
4949
InstanceSocketDir = "sock"
50+
51+
// InstanceConfigDir is the default location for config files with a relative path in HostFile
52+
InstanceConfigDir = "conf"
5053
)
5154

5255
// Filenames used under a disk directory

0 commit comments

Comments
 (0)