Skip to content

Commit d67e745

Browse files
committed
pkg/login1: initial commit
This commit adds login1 package, which is a small subset of github.com/coreos/go-systemd/v22/login1 package with ability to use shared D-Bus connection and with proper error handling for Reboot method call, which is not yet provided by the upstream. The idea is to use this package in favor of github.com/coreos/go-systemd in agent code responsible for rebooting the node. However, this requires tests in agent code, so it will be done in the next step. See coreos/go-systemd#387 for more details. Signed-off-by: Mateusz Gozdek <[email protected]>
1 parent 64b7f88 commit d67e745

File tree

2 files changed

+231
-0
lines changed

2 files changed

+231
-0
lines changed

pkg/login1/login1.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Package login1 is a small subset of github.com/coreos/go-systemd/v22/login1 package with
2+
// ability to use shared D-Bus connection and with proper error handling for Reboot method call, which
3+
// is not yet provided by the upstream.
4+
package login1
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
godbus "github.com/godbus/dbus/v5"
11+
)
12+
13+
// Client describes functionality of provided login1 client.
14+
type Client interface {
15+
Reboot(context.Context) error
16+
}
17+
18+
// Objector describes functionality required from a given D-Bus connection.
19+
type Objector interface {
20+
Object(string, godbus.ObjectPath) godbus.BusObject
21+
}
22+
23+
// Caller describes required functionality from D-Bus object.
24+
type Caller interface {
25+
CallWithContext(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call
26+
}
27+
28+
// New creates new login1 client using given D-Bus connection.
29+
func New(objector Objector) (Client, error) {
30+
if objector == nil {
31+
return nil, fmt.Errorf("no objector given")
32+
}
33+
34+
// Object path used by systemd-logind.
35+
dbusDest := "org.freedesktop.login1"
36+
37+
// Standard path to systemd-logind interface.
38+
dbusPath := godbus.ObjectPath("/org/freedesktop/login1")
39+
40+
return &rebooter{
41+
caller: objector.Object(dbusDest, dbusPath),
42+
}, nil
43+
}
44+
45+
// Reboot reboots machine on which it's called.
46+
func (r *rebooter) Reboot(ctx context.Context) error {
47+
// Systemd-logind interface name.
48+
dbusInterface := "org.freedesktop.login1.Manager"
49+
50+
// Login1 manager interface method name responsible for rebooting.
51+
dbusMethodNameReboot := "Reboot"
52+
53+
if call := r.caller.CallWithContext(ctx, dbusInterface+"."+dbusMethodNameReboot, 0, false); call.Err != nil {
54+
return fmt.Errorf("calling reboot: %w", call.Err)
55+
}
56+
57+
return nil
58+
}
59+
60+
// Rebooter is an internal type implementing Client interface.
61+
type rebooter struct {
62+
caller Caller
63+
}
64+
65+
// Rebooter must implement Client interface.
66+
var _ Client = &rebooter{}

pkg/login1/login1_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package login1_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"testing"
8+
9+
godbus "github.com/godbus/dbus/v5"
10+
11+
"github.com/flatcar-linux/flatcar-linux-update-operator/pkg/dbus"
12+
"github.com/flatcar-linux/flatcar-linux-update-operator/pkg/login1"
13+
)
14+
15+
func Test_Creating_new_client(t *testing.T) {
16+
t.Parallel()
17+
18+
t.Run("connects_to_global_login1_path_and_interface", func(t *testing.T) {
19+
t.Parallel()
20+
21+
objectConstructorCalled := false
22+
23+
connectionWithContextCheck := &dbus.MockConnection{
24+
ObjectF: func(dest string, path godbus.ObjectPath) godbus.BusObject {
25+
objectConstructorCalled = true
26+
27+
expectedDest := "org.freedesktop.login1"
28+
29+
if dest != expectedDest {
30+
t.Fatalf("Expected D-Bus destination %q, got %q", expectedDest, dest)
31+
}
32+
33+
expectedPath := godbus.ObjectPath("/org/freedesktop/login1")
34+
35+
if path != expectedPath {
36+
t.Fatalf("Expected D-Bus path %q, got %q", expectedPath, path)
37+
}
38+
39+
return nil
40+
},
41+
}
42+
43+
if _, err := login1.New(connectionWithContextCheck); err != nil {
44+
t.Fatalf("Unexpected error creating client: %v", err)
45+
}
46+
47+
if !objectConstructorCalled {
48+
t.Fatalf("Expected object constructor to be called")
49+
}
50+
})
51+
52+
t.Run("returns_error_when_no_objector_is_given", func(t *testing.T) {
53+
t.Parallel()
54+
55+
client, err := login1.New(nil)
56+
if err == nil {
57+
t.Fatalf("Expected error creating client with no connector")
58+
}
59+
60+
if client != nil {
61+
t.Fatalf("Expected client to be nil when New returns error")
62+
}
63+
})
64+
}
65+
66+
//nolint:funlen // Many subtests.
67+
func Test_Rebooting(t *testing.T) {
68+
t.Parallel()
69+
70+
t.Run("calls_login1_reboot_method_on_manager_interface", func(t *testing.T) {
71+
t.Parallel()
72+
73+
rebootCalled := false
74+
75+
connectionWithContextCheck := &dbus.MockConnection{
76+
ObjectF: func(string, godbus.ObjectPath) godbus.BusObject {
77+
return &dbus.MockObject{
78+
CallWithContextF: func(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call {
79+
rebootCalled = true
80+
81+
expectedMethodName := "org.freedesktop.login1.Manager.Reboot"
82+
83+
if method != expectedMethodName {
84+
t.Fatalf("Expected method %q being called, got %q", expectedMethodName, method)
85+
}
86+
87+
return &godbus.Call{}
88+
},
89+
}
90+
},
91+
}
92+
93+
client, err := login1.New(connectionWithContextCheck)
94+
if err != nil {
95+
t.Fatalf("Unexpected error creating client: %v", err)
96+
}
97+
98+
if err := client.Reboot(context.Background()); err != nil {
99+
t.Fatalf("Unexpected error rebooting: %v", err)
100+
}
101+
102+
if !rebootCalled {
103+
t.Fatalf("Expected reboot method call on given D-Bus connection")
104+
}
105+
})
106+
107+
t.Run("use_given_context_for_D-Bus_call", func(t *testing.T) {
108+
t.Parallel()
109+
110+
testKey := struct{}{}
111+
expectedValue := "bar"
112+
113+
ctx := context.WithValue(context.Background(), testKey, expectedValue)
114+
115+
connectionWithContextCheck := &dbus.MockConnection{
116+
ObjectF: func(string, godbus.ObjectPath) godbus.BusObject {
117+
return &dbus.MockObject{
118+
CallWithContextF: func(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call {
119+
if val := ctx.Value(testKey); val != expectedValue {
120+
t.Fatalf("Got unexpected context on call")
121+
}
122+
123+
return &godbus.Call{}
124+
},
125+
}
126+
},
127+
}
128+
129+
client, err := login1.New(connectionWithContextCheck)
130+
if err != nil {
131+
t.Fatalf("Unexpected error creating client: %v", err)
132+
}
133+
134+
if err := client.Reboot(ctx); err != nil {
135+
t.Fatalf("Unexpected error rebooting: %v", err)
136+
}
137+
})
138+
139+
t.Run("returns_error_when_D-Bus_call_fails", func(t *testing.T) {
140+
t.Parallel()
141+
142+
expectedError := fmt.Errorf("reboot error")
143+
144+
connectionWithFailingObjectCall := &dbus.MockConnection{
145+
ObjectF: func(string, godbus.ObjectPath) godbus.BusObject {
146+
return &dbus.MockObject{
147+
CallWithContextF: func(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call {
148+
return &godbus.Call{
149+
Err: expectedError,
150+
}
151+
},
152+
}
153+
},
154+
}
155+
156+
client, err := login1.New(connectionWithFailingObjectCall)
157+
if err != nil {
158+
t.Fatalf("Unexpected error creating client: %v", err)
159+
}
160+
161+
if err := client.Reboot(context.Background()); !errors.Is(err, expectedError) {
162+
t.Fatalf("Unexpected error rebooting: %v", err)
163+
}
164+
})
165+
}

0 commit comments

Comments
 (0)