Skip to content

Commit 6aae2cf

Browse files
committed
fix: env vars in exec and systemd mode
1 parent c539d3c commit 6aae2cf

File tree

5 files changed

+388
-4
lines changed

5 files changed

+388
-4
lines changed

.air.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ tmp_dir = "tmp"
55
[build]
66
args_bin = []
77
bin = "./tmp/main"
8-
cmd = "go build -tags containers_image_openpgp -o ./tmp/main ./cmd/api"
8+
cmd = "make build-embedded && go build -tags containers_image_openpgp -o ./tmp/main ./cmd/api"
99
delay = 1000
1010
exclude_dir = ["assets", "tmp", "vendor", "testdata", "bin", "scripts", "data", "kernel"]
1111
exclude_file = []

lib/instances/manager_test.go

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,14 @@ func TestBasicEndToEnd(t *testing.T) {
675675
t.Logf("Volume test output:\n%s", output)
676676
t.Log("Volume read/write test passed!")
677677

678+
// Test environment variables are accessible via exec (tests guest-agent has env vars)
679+
t.Log("Testing environment variables via exec...")
680+
output, exitCode, err = runCmd("printenv", "TEST_VAR")
681+
require.NoError(t, err, "printenv should execute")
682+
require.Equal(t, 0, exitCode, "printenv should succeed")
683+
assert.Equal(t, "test_value", strings.TrimSpace(output), "Environment variable should be accessible via exec")
684+
t.Log("Environment variable accessible via exec!")
685+
678686
// Test streaming logs with live updates
679687
t.Log("Testing log streaming with live updates...")
680688
streamCtx, streamCancel := context.WithCancel(ctx)
@@ -747,6 +755,168 @@ func TestBasicEndToEnd(t *testing.T) {
747755
t.Log("Instance and volume lifecycle test complete!")
748756
}
749757

758+
// TestEntrypointEnvVars verifies that environment variables are passed to the entrypoint process.
759+
// This uses bitnami/redis which configures REDIS_PASSWORD from an env var - if auth is required,
760+
// it proves the entrypoint received and used the env var.
761+
func TestEntrypointEnvVars(t *testing.T) {
762+
if os.Getuid() != 0 {
763+
t.Skip("Skipping test that requires root")
764+
}
765+
766+
mgr, tmpDir := setupTestManager(t) // Automatically registers cleanup
767+
ctx := context.Background()
768+
769+
// Get image manager
770+
p := paths.New(tmpDir)
771+
imageManager, err := images.NewManager(p, 1, nil)
772+
require.NoError(t, err)
773+
774+
// Pull bitnami/redis image
775+
t.Log("Pulling bitnami/redis image...")
776+
redisImage, err := imageManager.CreateImage(ctx, images.CreateImageRequest{
777+
Name: "docker.io/bitnami/redis:latest",
778+
})
779+
require.NoError(t, err)
780+
781+
// Wait for image to be ready
782+
t.Log("Waiting for image build to complete...")
783+
imageName := redisImage.Name
784+
for i := 0; i < 60; i++ {
785+
img, err := imageManager.GetImage(ctx, imageName)
786+
if err == nil && img.Status == images.StatusReady {
787+
redisImage = img
788+
break
789+
}
790+
if err == nil && img.Status == images.StatusFailed {
791+
t.Fatalf("Image build failed: %s", *img.Error)
792+
}
793+
time.Sleep(1 * time.Second)
794+
}
795+
require.Equal(t, images.StatusReady, redisImage.Status, "Image should be ready after 60 seconds")
796+
t.Log("Redis image ready")
797+
798+
// Ensure system files
799+
systemManager := system.NewManager(p)
800+
t.Log("Ensuring system files...")
801+
err = systemManager.EnsureSystemFiles(ctx)
802+
require.NoError(t, err)
803+
t.Log("System files ready")
804+
805+
// Initialize network (needed for loopback interface in guest)
806+
networkManager := network.NewManager(p, &config.Config{
807+
DataDir: tmpDir,
808+
BridgeName: "vmbr0",
809+
SubnetCIDR: "10.100.0.0/16",
810+
DNSServer: "1.1.1.1",
811+
}, nil)
812+
t.Log("Initializing network...")
813+
err = networkManager.Initialize(ctx, nil)
814+
require.NoError(t, err)
815+
t.Log("Network initialized")
816+
817+
// Create instance with REDIS_PASSWORD env var
818+
testPassword := "test_secret_password_123"
819+
req := CreateInstanceRequest{
820+
Name: "test-redis-env",
821+
Image: "docker.io/bitnami/redis:latest",
822+
Size: 2 * 1024 * 1024 * 1024,
823+
HotplugSize: 512 * 1024 * 1024,
824+
OverlaySize: 10 * 1024 * 1024 * 1024,
825+
Vcpus: 2,
826+
NetworkEnabled: true, // Need network for loopback to work properly
827+
Env: map[string]string{
828+
"REDIS_PASSWORD": testPassword,
829+
},
830+
}
831+
832+
t.Log("Creating redis instance with REDIS_PASSWORD...")
833+
inst, err := mgr.CreateInstance(ctx, req)
834+
require.NoError(t, err)
835+
require.NotNil(t, inst)
836+
assert.Equal(t, StateRunning, inst.State)
837+
t.Logf("Instance created: %s", inst.Id)
838+
839+
// Wait for redis to be ready (bitnami/redis takes longer to start)
840+
t.Log("Waiting for redis to be ready...")
841+
time.Sleep(15 * time.Second)
842+
843+
// Helper to run command in guest with retry
844+
runCmd := func(command ...string) (string, int, error) {
845+
var lastOutput string
846+
var lastExitCode int
847+
var lastErr error
848+
849+
dialer, err := hypervisor.NewVsockDialer(inst.HypervisorType, inst.VsockSocket, inst.VsockCID)
850+
if err != nil {
851+
return "", -1, err
852+
}
853+
854+
for attempt := 0; attempt < 5; attempt++ {
855+
if attempt > 0 {
856+
time.Sleep(200 * time.Millisecond)
857+
}
858+
859+
var stdout, stderr bytes.Buffer
860+
exit, err := guest.ExecIntoInstance(ctx, dialer, guest.ExecOptions{
861+
Command: command,
862+
Stdout: &stdout,
863+
Stderr: &stderr,
864+
TTY: false,
865+
})
866+
867+
output := stdout.String()
868+
if stderr.Len() > 0 {
869+
output += stderr.String()
870+
}
871+
output = strings.TrimSpace(output)
872+
873+
if err != nil {
874+
lastErr = err
875+
lastOutput = output
876+
lastExitCode = -1
877+
continue
878+
}
879+
880+
lastOutput = output
881+
lastExitCode = exit.Code
882+
lastErr = nil
883+
884+
if output != "" || exit.Code == 0 {
885+
return output, exit.Code, nil
886+
}
887+
}
888+
889+
return lastOutput, lastExitCode, lastErr
890+
}
891+
892+
// Test 1: PING without auth should fail
893+
t.Log("Testing redis PING without auth (should fail)...")
894+
output, _, err := runCmd("redis-cli", "PING")
895+
require.NoError(t, err)
896+
assert.Contains(t, output, "NOAUTH", "Redis should require authentication")
897+
898+
// Test 2: PING with correct password should succeed
899+
t.Log("Testing redis PING with correct password (should succeed)...")
900+
output, exitCode, err := runCmd("redis-cli", "-a", testPassword, "PING")
901+
require.NoError(t, err)
902+
require.Equal(t, 0, exitCode)
903+
assert.Contains(t, output, "PONG", "Redis should respond to authenticated PING")
904+
905+
// Test 3: Verify requirepass config matches our env var
906+
t.Log("Verifying redis requirepass config...")
907+
output, exitCode, err = runCmd("redis-cli", "-a", testPassword, "CONFIG", "GET", "requirepass")
908+
require.NoError(t, err)
909+
require.Equal(t, 0, exitCode)
910+
assert.Contains(t, output, testPassword, "Redis requirepass should match REDIS_PASSWORD env var")
911+
912+
t.Log("Entrypoint environment variable test passed!")
913+
914+
// Cleanup
915+
t.Log("Cleaning up...")
916+
err = mgr.DeleteInstance(ctx, inst.Id)
917+
require.NoError(t, err)
918+
}
919+
750920
func TestStorageOperations(t *testing.T) {
751921
// Test storage layer without starting VMs
752922
tmpDir := t.TempDir()
@@ -1001,4 +1171,3 @@ func (r *testInstanceResolver) ResolveInstance(ctx context.Context, nameOrID str
10011171
// For tests, just return nameOrID as both name and id
10021172
return nameOrID, nameOrID, nil
10031173
}
1004-

lib/instances/qemu_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,14 @@ func TestQEMUBasicEndToEnd(t *testing.T) {
507507
t.Logf("Volume test output:\n%s", output)
508508
t.Log("Volume read/write test passed!")
509509

510+
// Test environment variables are accessible via exec (tests guest-agent has env vars)
511+
t.Log("Testing environment variables via exec...")
512+
output, exitCode, err = runCmd("printenv", "TEST_VAR")
513+
require.NoError(t, err, "printenv should execute")
514+
require.Equal(t, 0, exitCode, "printenv should succeed")
515+
assert.Equal(t, "test_value", strings.TrimSpace(output), "Environment variable should be accessible via exec")
516+
t.Log("Environment variable accessible via exec!")
517+
510518
// Delete instance
511519
t.Log("Deleting instance...")
512520
err = manager.DeleteInstance(ctx, inst.Id)
@@ -537,6 +545,175 @@ func TestQEMUBasicEndToEnd(t *testing.T) {
537545
t.Log("QEMU instance lifecycle test complete!")
538546
}
539547

548+
// TestQEMUEntrypointEnvVars verifies that environment variables are passed to the entrypoint process.
549+
// This uses bitnami/redis which configures REDIS_PASSWORD from an env var - if auth is required,
550+
// it proves the entrypoint received and used the env var.
551+
func TestQEMUEntrypointEnvVars(t *testing.T) {
552+
if os.Getuid() != 0 {
553+
t.Skip("Skipping test that requires root")
554+
}
555+
556+
// Require QEMU to be installed
557+
starter := qemu.NewStarter()
558+
if _, err := starter.GetBinaryPath(nil, ""); err != nil {
559+
t.Fatalf("QEMU not available: %v", err)
560+
}
561+
562+
mgr, tmpDir := setupTestManagerForQEMU(t)
563+
ctx := context.Background()
564+
565+
// Get image manager
566+
p := paths.New(tmpDir)
567+
imageManager, err := images.NewManager(p, 1, nil)
568+
require.NoError(t, err)
569+
570+
// Pull bitnami/redis image
571+
t.Log("Pulling bitnami/redis image...")
572+
redisImage, err := imageManager.CreateImage(ctx, images.CreateImageRequest{
573+
Name: "docker.io/bitnami/redis:latest",
574+
})
575+
require.NoError(t, err)
576+
577+
// Wait for image to be ready
578+
t.Log("Waiting for image build to complete...")
579+
imageName := redisImage.Name
580+
for i := 0; i < 60; i++ {
581+
img, err := imageManager.GetImage(ctx, imageName)
582+
if err == nil && img.Status == images.StatusReady {
583+
redisImage = img
584+
break
585+
}
586+
if err == nil && img.Status == images.StatusFailed {
587+
t.Fatalf("Image build failed: %s", *img.Error)
588+
}
589+
time.Sleep(1 * time.Second)
590+
}
591+
require.Equal(t, images.StatusReady, redisImage.Status, "Image should be ready after 60 seconds")
592+
t.Log("Redis image ready")
593+
594+
// Ensure system files
595+
systemManager := system.NewManager(p)
596+
t.Log("Ensuring system files...")
597+
err = systemManager.EnsureSystemFiles(ctx)
598+
require.NoError(t, err)
599+
t.Log("System files ready")
600+
601+
// Initialize network (needed for loopback interface in guest)
602+
networkManager := network.NewManager(p, &config.Config{
603+
DataDir: tmpDir,
604+
BridgeName: "vmbr0",
605+
SubnetCIDR: "10.100.0.0/16",
606+
DNSServer: "1.1.1.1",
607+
}, nil)
608+
t.Log("Initializing network...")
609+
err = networkManager.Initialize(ctx, nil)
610+
require.NoError(t, err)
611+
t.Log("Network initialized")
612+
613+
// Create instance with REDIS_PASSWORD env var
614+
testPassword := "test_secret_password_123"
615+
req := CreateInstanceRequest{
616+
Name: "test-redis-env",
617+
Image: "docker.io/bitnami/redis:latest",
618+
Size: 2 * 1024 * 1024 * 1024,
619+
HotplugSize: 512 * 1024 * 1024,
620+
OverlaySize: 10 * 1024 * 1024 * 1024,
621+
Vcpus: 2,
622+
NetworkEnabled: true, // Need network for loopback to work properly
623+
Env: map[string]string{
624+
"REDIS_PASSWORD": testPassword,
625+
},
626+
}
627+
628+
t.Log("Creating redis instance with REDIS_PASSWORD...")
629+
inst, err := mgr.CreateInstance(ctx, req)
630+
require.NoError(t, err)
631+
require.NotNil(t, inst)
632+
assert.Equal(t, StateRunning, inst.State)
633+
assert.Equal(t, hypervisor.TypeQEMU, inst.HypervisorType, "Instance should use QEMU hypervisor")
634+
t.Logf("Instance created: %s", inst.Id)
635+
636+
// Wait for redis to be ready (bitnami/redis takes longer to start)
637+
t.Log("Waiting for redis to be ready...")
638+
time.Sleep(15 * time.Second)
639+
640+
// Helper to run command in guest with retry
641+
runCmd := func(command ...string) (string, int, error) {
642+
var lastOutput string
643+
var lastExitCode int
644+
var lastErr error
645+
646+
dialer, err := hypervisor.NewVsockDialer(inst.HypervisorType, inst.VsockSocket, inst.VsockCID)
647+
if err != nil {
648+
return "", -1, err
649+
}
650+
651+
for attempt := 0; attempt < 5; attempt++ {
652+
if attempt > 0 {
653+
time.Sleep(200 * time.Millisecond)
654+
}
655+
656+
var stdout, stderr bytes.Buffer
657+
exit, err := guest.ExecIntoInstance(ctx, dialer, guest.ExecOptions{
658+
Command: command,
659+
Stdout: &stdout,
660+
Stderr: &stderr,
661+
TTY: false,
662+
})
663+
664+
output := stdout.String()
665+
if stderr.Len() > 0 {
666+
output += stderr.String()
667+
}
668+
output = strings.TrimSpace(output)
669+
670+
if err != nil {
671+
lastErr = err
672+
lastOutput = output
673+
lastExitCode = -1
674+
continue
675+
}
676+
677+
lastOutput = output
678+
lastExitCode = exit.Code
679+
lastErr = nil
680+
681+
if output != "" || exit.Code == 0 {
682+
return output, exit.Code, nil
683+
}
684+
}
685+
686+
return lastOutput, lastExitCode, lastErr
687+
}
688+
689+
// Test 1: PING without auth should fail
690+
t.Log("Testing redis PING without auth (should fail)...")
691+
output, _, err := runCmd("redis-cli", "PING")
692+
require.NoError(t, err)
693+
assert.Contains(t, output, "NOAUTH", "Redis should require authentication")
694+
695+
// Test 2: PING with correct password should succeed
696+
t.Log("Testing redis PING with correct password (should succeed)...")
697+
output, exitCode, err := runCmd("redis-cli", "-a", testPassword, "PING")
698+
require.NoError(t, err)
699+
require.Equal(t, 0, exitCode)
700+
assert.Contains(t, output, "PONG", "Redis should respond to authenticated PING")
701+
702+
// Test 3: Verify requirepass config matches our env var
703+
t.Log("Verifying redis requirepass config...")
704+
output, exitCode, err = runCmd("redis-cli", "-a", testPassword, "CONFIG", "GET", "requirepass")
705+
require.NoError(t, err)
706+
require.Equal(t, 0, exitCode)
707+
assert.Contains(t, output, testPassword, "Redis requirepass should match REDIS_PASSWORD env var")
708+
709+
t.Log("QEMU entrypoint environment variable test passed!")
710+
711+
// Cleanup
712+
t.Log("Cleaning up...")
713+
err = mgr.DeleteInstance(ctx, inst.Id)
714+
require.NoError(t, err)
715+
}
716+
540717
// TestQEMUStandbyAndRestore tests the standby/restore cycle with QEMU.
541718
// This tests QEMU's migrate-to-file snapshot mechanism.
542719
func TestQEMUStandbyAndRestore(t *testing.T) {

0 commit comments

Comments
 (0)