diff --git a/go.mod b/go.mod index 0f4f2e9b3..eac7d5ac2 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.17 require ( github.com/blang/semver/v4 v4.0.0 - github.com/coreos/go-systemd/v22 v22.3.2 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f github.com/flatcar-linux/locksmith v0.6.2 github.com/godbus/dbus/v5 v5.0.6 diff --git a/go.sum b/go.sum index b1ae7cfc7..bd9e26f63 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -73,7 +71,6 @@ github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index ae4a758e8..fa124e4ab 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -14,7 +14,6 @@ import ( "sync" "time" - "github.com/coreos/go-systemd/v22/login1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -27,7 +26,9 @@ import ( "k8s.io/klog/v2" "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/constants" + "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/dbus" "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/k8sutil" + "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/login1" "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/updateengine" ) @@ -37,7 +38,7 @@ type Klocksmith struct { kc kubernetes.Interface nc corev1client.NodeInterface ue updateengine.Client - lc *login1.Conn + lc Rebooter reapTimeout time.Duration } @@ -55,6 +56,10 @@ var shouldRebootSelector = fields.Set(map[string]string{ constants.AnnotationRebootNeeded: constants.True, }).AsSelector() +type Rebooter interface { + Reboot(ctx context.Context) error +} + // New returns initialized Klocksmith. func New(node string, reapTimeout time.Duration) (*Klocksmith, error) { // Set up kubernetes in-cluster client. @@ -66,14 +71,19 @@ func New(node string, reapTimeout time.Duration) (*Klocksmith, error) { // Node interface. nc := kc.CoreV1().Nodes() + dbusClient, err := dbus.New(dbus.SystemPrivateConnector) + if err != nil { + return nil, fmt.Errorf("creating D-Bus client: %w", err) + } + // Set up update_engine client. - updateEngineClient, err := updateengine.New(updateengine.DBusSystemPrivateConnector) + updateEngineClient, err := updateengine.New(dbusClient) if err != nil { return nil, fmt.Errorf("establishing connection to update_engine dbus: %w", err) } // Set up login1 client for our eventual reboot. - lc, err := login1.New() + lc, err := login1.New(dbusClient) if err != nil { return nil, fmt.Errorf("establishing connection to logind dbus: %w", err) } @@ -267,7 +277,9 @@ func (k *Klocksmith) process(stop <-chan struct{}) error { klog.Info("Node drained, rebooting") // Reboot. - k.lc.Reboot(false) + if err := k.lc.Reboot(context.TODO()); err != nil { + return fmt.Errorf("rebooting: %w", err) + } // Cross fingers. sleepOrDone(24*7*time.Hour, stop) diff --git a/pkg/dbus/conn.go b/pkg/dbus/conn.go new file mode 100644 index 000000000..c5339c280 --- /dev/null +++ b/pkg/dbus/conn.go @@ -0,0 +1,64 @@ +// Package dbus provides a helper function for creating new D-Bus client. +package dbus + +import ( + "fmt" + "os" + "strconv" + + godbus "github.com/godbus/dbus/v5" +) + +// Client is an interface describing capabilities of internal D-Bus client. +type Client interface { + AddMatchSignal(matchOptions ...godbus.MatchOption) error + Signal(ch chan<- *godbus.Signal) + Object(dest string, path godbus.ObjectPath) godbus.BusObject + Close() error +} + +// Connection is an interface describing how much functionality we need from object providing D-Bus connection. +type Connection interface { + Auth(authMethods []godbus.Auth) error + Hello() error + + Client +} + +// Connector is a constructor function providing D-Bus connection. +type Connector func() (Connection, error) + +// SystemPrivateConnector is a standard connector using system bus. +func SystemPrivateConnector() (Connection, error) { + return godbus.SystemBusPrivate() +} + +// New creates new D-Bus client using given connector. +func New(connector Connector) (Client, error) { + if connector == nil { + return nil, fmt.Errorf("no connection creator given") + } + + conn, err := connector() + if err != nil { + return nil, fmt.Errorf("connecting to D-Bus: %w", err) + } + + methods := []godbus.Auth{godbus.AuthExternal(strconv.Itoa(os.Getuid()))} + + if err := conn.Auth(methods); err != nil { + // Best effort closing the connection. + _ = conn.Close() + + return nil, fmt.Errorf("authenticating to D-Bus: %w", err) + } + + if err := conn.Hello(); err != nil { + // Best effort closing the connection. + _ = conn.Close() + + return nil, fmt.Errorf("sending hello to D-Bus: %w", err) + } + + return conn, nil +} diff --git a/pkg/dbus/conn_integration_test.go b/pkg/dbus/conn_integration_test.go new file mode 100644 index 000000000..fcfbe457a --- /dev/null +++ b/pkg/dbus/conn_integration_test.go @@ -0,0 +1,30 @@ +//go:build integration +// +build integration + +package dbus_test + +import ( + "fmt" + "os" + "testing" + + "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/dbus" +) + +const ( + testDbusSocketEnv = "FLUO_TEST_DBUS_SOCKET" +) + +//nolint:paralleltest // This test use environment variables. +func Test_System_private_connector_successfully_connects_to_running_system_bus(t *testing.T) { + t.Setenv("DBUS_SYSTEM_BUS_ADDRESS", fmt.Sprintf("unix:path=%s", os.Getenv(testDbusSocketEnv))) + + client, err := dbus.New(dbus.SystemPrivateConnector) + if err != nil { + t.Fatalf("Failed creating client: %v", err) + } + + if client == nil { + t.Fatalf("Expected not nil client when new succeeds") + } +} diff --git a/pkg/dbus/conn_test.go b/pkg/dbus/conn_test.go new file mode 100644 index 000000000..8e5e8203a --- /dev/null +++ b/pkg/dbus/conn_test.go @@ -0,0 +1,189 @@ +package dbus_test + +import ( + "encoding/hex" + "errors" + "fmt" + "os" + "strconv" + "testing" + + godbus "github.com/godbus/dbus/v5" + + "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/dbus" +) + +func Test_Creating_client_authenticates_using_user_id(t *testing.T) { + t.Parallel() + + authCheckingConnection := &mockConnection{ + authF: func(authMethods []godbus.Auth) error { + uidAuthFound := false + + for i, method := range authMethods { + _, data, _ := method.FirstData() + + decodedData, err := hex.DecodeString(string(data)) + if err != nil { + t.Fatalf("Received auth method %d has bad hex data %v: %v", i, data, err) + } + + potentialUID, err := strconv.Atoi(string(decodedData)) + if err != nil { + t.Logf("Data %q couldn't be converted to UID: %v", string(decodedData), err) + } + + if potentialUID == os.Getuid() { + uidAuthFound = true + } + } + + if !uidAuthFound { + t.Fatalf("Expected auth method with user id") + } + + return nil + }, + } + + client, err := dbus.New(func() (dbus.Connection, error) { return authCheckingConnection, nil }) + if err != nil { + t.Fatalf("Unexpected error creating client: %v", err) + } + + if client == nil { + t.Fatalf("When new succeeds, returned client should not be nil") + } +} + +//nolint:funlen // Just many subtests. +func Test_Creating_client_returns_error_when(t *testing.T) { + t.Parallel() + + t.Run("no_connector_is_given", func(t *testing.T) { + t.Parallel() + + testNewError(t, nil, nil) + }) + + t.Run("connecting_to_D-Bus_socket_fails", func(t *testing.T) { + t.Parallel() + + expectedErr := fmt.Errorf("connection error") + + failingConnectionConnector := func() (dbus.Connection, error) { return nil, expectedErr } + + testNewError(t, failingConnectionConnector, expectedErr) + }) + + t.Run("authenticating_to_D-Bus_fails", func(t *testing.T) { + t.Parallel() + + expectedErr := fmt.Errorf("auth error") + + closeCalled := false + + failingAuthConnection := &mockConnection{ + authF: func([]godbus.Auth) error { + return expectedErr + }, + closeF: func() error { + closeCalled = true + + return fmt.Errorf("closing error") + }, + } + + testNewError(t, func() (dbus.Connection, error) { return failingAuthConnection, nil }, expectedErr) + + t.Run("and_tries_to_close_the_client_while_ignoring_closing_error", func(t *testing.T) { + if !closeCalled { + t.Fatalf("Expected close function to be called") + } + }) + }) + + t.Run("sending_hello_to_D-Bus_fails", func(t *testing.T) { + t.Parallel() + + expectedErr := fmt.Errorf("hello error") + + closeCalled := false + + failingHelloConnection := &mockConnection{ + helloF: func() error { + return expectedErr + }, + closeF: func() error { + closeCalled = true + + return fmt.Errorf("closing error") + }, + } + + testNewError(t, func() (dbus.Connection, error) { return failingHelloConnection, nil }, expectedErr) + + t.Run("and_tries_to_close_the_client_while_ignoring_closing_error", func(t *testing.T) { + if !closeCalled { + t.Fatalf("Expected close function to be called") + } + }) + }) +} + +func testNewError(t *testing.T, connector dbus.Connector, expectedErr error) { + t.Helper() + + client, err := dbus.New(connector) + if err == nil { + t.Fatalf("Expected error creating client") + } + + if client != nil { + t.Fatalf("Client should not be returned when creation error occurs") + } + + if expectedErr != nil && !errors.Is(err, expectedErr) { + t.Fatalf("Unexpected error occurred, expected %q, got %q", expectedErr, err) + } +} + +type mockConnection struct { + authF func(authMethods []godbus.Auth) error + helloF func() error + closeF func() error +} + +func (m *mockConnection) Auth(authMethods []godbus.Auth) error { + if m.authF == nil { + return nil + } + + return m.authF(authMethods) +} + +func (m *mockConnection) Hello() error { + if m.helloF == nil { + return nil + } + + return m.helloF() +} + +func (m *mockConnection) AddMatchSignal(matchOptions ...godbus.MatchOption) error { + return nil +} + +func (m *mockConnection) Signal(ch chan<- *godbus.Signal) {} + +func (m *mockConnection) Object(dest string, path godbus.ObjectPath) godbus.BusObject { + return nil +} + +func (m *mockConnection) Close() error { + if m.closeF == nil { + return nil + } + + return m.closeF() +} diff --git a/pkg/dbus/mock.go b/pkg/dbus/mock.go new file mode 100644 index 000000000..9fc3fced6 --- /dev/null +++ b/pkg/dbus/mock.go @@ -0,0 +1,134 @@ +package dbus + +import ( + "context" + + godbus "github.com/godbus/dbus/v5" +) + +// MockConnection is a test helper for mocking godbus.Conn. +type MockConnection struct { + AuthF func(authMethods []godbus.Auth) error + HelloF func() error + CloseF func() error + AddMatchSignalF func(...godbus.MatchOption) error + SignalF func(chan<- *godbus.Signal) + ObjectF func(string, godbus.ObjectPath) godbus.BusObject +} + +// Auth ... +func (m *MockConnection) Auth(authMethods []godbus.Auth) error { + if m.AuthF == nil { + return nil + } + + return m.AuthF(authMethods) +} + +// Hello ... +func (m *MockConnection) Hello() error { + if m.HelloF == nil { + return nil + } + + return m.HelloF() +} + +// AddMatchSignal ... +func (m *MockConnection) AddMatchSignal(matchOptions ...godbus.MatchOption) error { + if m.AddMatchSignalF == nil { + return nil + } + + return m.AddMatchSignalF(matchOptions...) +} + +// Signal ... +func (m *MockConnection) Signal(ch chan<- *godbus.Signal) { + if m.SignalF == nil { + return + } + + m.SignalF(ch) +} + +// Object ... +func (m *MockConnection) Object(dest string, path godbus.ObjectPath) godbus.BusObject { + if m.ObjectF == nil { + return nil + } + + return m.ObjectF(dest, path) +} + +// Close ... +func (m *MockConnection) Close() error { + if m.CloseF == nil { + return nil + } + + return m.CloseF() +} + +// MockObject is a mock of godbus.BusObject. +type MockObject struct { + CallWithContextF func(context.Context, string, godbus.Flags, ...interface{}) *godbus.Call + CallF func(string, godbus.Flags, ...interface{}) *godbus.Call +} + +// CallWithContext ... +// +//nolint:lll // Upstream signature, can't do much with that. +func (m *MockObject) CallWithContext(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + if m.CallWithContextF == nil { + return &godbus.Call{} + } + + return m.CallWithContextF(ctx, method, flags, args...) +} + +// Call ... +func (m *MockObject) Call(method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + if m.CallF == nil { + return &godbus.Call{} + } + + return m.CallF(method, flags, args...) +} + +// Go ... +func (m *MockObject) Go(method string, flags godbus.Flags, ch chan *godbus.Call, args ...interface{}) *godbus.Call { + return &godbus.Call{} +} + +// GoWithContext ... +// +//nolint:lll // Upstream signature, can't do much with that. +func (m *MockObject) GoWithContext(ctx context.Context, method string, flags godbus.Flags, ch chan *godbus.Call, args ...interface{}) *godbus.Call { + return &godbus.Call{} +} + +// AddMatchSignal ... +func (m *MockObject) AddMatchSignal(iface, member string, options ...godbus.MatchOption) *godbus.Call { + return &godbus.Call{} +} + +// RemoveMatchSignal ... +func (m *MockObject) RemoveMatchSignal(iface, member string, options ...godbus.MatchOption) *godbus.Call { + return &godbus.Call{} +} + +// GetProperty ... +func (m *MockObject) GetProperty(p string) (godbus.Variant, error) { return godbus.Variant{}, nil } + +// StoreProperty ... +func (m *MockObject) StoreProperty(p string, value interface{}) error { return nil } + +// SetProperty ... +func (m *MockObject) SetProperty(p string, v interface{}) error { return nil } + +// Destination ... +func (m *MockObject) Destination() string { return "" } + +// Path ... +func (m *MockObject) Path() godbus.ObjectPath { return "" } diff --git a/pkg/login1/login1.go b/pkg/login1/login1.go new file mode 100644 index 000000000..bc82bb73a --- /dev/null +++ b/pkg/login1/login1.go @@ -0,0 +1,61 @@ +// Package login1 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. +package login1 + +import ( + "context" + "fmt" + + godbus "github.com/godbus/dbus/v5" +) + +const ( + // DBusDest is an object path used by systemd-logind. + DBusDest = "org.freedesktop.login1" + // DBusInterface is an systemd-logind intefrace name. + DBusInterface = "org.freedesktop.login1.Manager" + // DBusPath is a standard path to systemd-logind interface. + DBusPath = "/org/freedesktop/login1" + // DBusMethodNameReboot is a login1 manager interface method name responsible for rebooting. + DBusMethodNameReboot = "Reboot" +) + +// Client describes functionality of provided login1 client. +type Client interface { + Reboot(context.Context) error +} + +// Objector describes functionality required from given D-Bus connection. +type Objector interface { + Object(string, godbus.ObjectPath) godbus.BusObject +} + +// Caller describes required functionality from D-Bus object. +type Caller interface { + CallWithContext(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call +} + +type rebooter struct { + caller Caller +} + +// New creates new login1 client using given D-Bus connection. +func New(objector Objector) (Client, error) { + if objector == nil { + return nil, fmt.Errorf("no objector given") + } + + return &rebooter{ + caller: objector.Object(DBusDest, DBusPath), + }, nil +} + +// Reboot reboots machine on which it's called. +func (r *rebooter) Reboot(ctx context.Context) error { + if call := r.caller.CallWithContext(ctx, DBusInterface+"."+DBusMethodNameReboot, 0, false); call.Err != nil { + return fmt.Errorf("calling reboot: %w", call.Err) + } + + return nil +} diff --git a/pkg/login1/login1_test.go b/pkg/login1/login1_test.go new file mode 100644 index 000000000..38e4f9695 --- /dev/null +++ b/pkg/login1/login1_test.go @@ -0,0 +1,89 @@ +package login1_test + +import ( + "context" + "errors" + "fmt" + "testing" + + godbus "github.com/godbus/dbus/v5" + + "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/dbus" + "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/login1" +) + +func Test_Creating_new_client_returns_error_when_no_objector_is_given(t *testing.T) { + t.Parallel() + + client, err := login1.New(nil) + if err == nil { + t.Fatalf("Expected error creating client with no connector") + } + + if client != nil { + t.Fatalf("Expected client to be nil when New returns error") + } +} + +func Test_Rebooting(t *testing.T) { + t.Parallel() + + t.Run("use_given_context_for_D-Bus_call", func(t *testing.T) { + t.Parallel() + + testKey := struct{}{} + expectedValue := "bar" + + ctx := context.WithValue(context.Background(), testKey, expectedValue) + + connectionWithContextCheck := &dbus.MockConnection{ + ObjectF: func(string, godbus.ObjectPath) godbus.BusObject { + return &dbus.MockObject{ + CallWithContextF: func(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + if val := ctx.Value(testKey); val != expectedValue { + t.Fatalf("Got unexpected context on call") + } + + return &godbus.Call{} + }, + } + }, + } + + client, err := login1.New(connectionWithContextCheck) + if err != nil { + t.Fatalf("Unexpected error creating client: %v", err) + } + + if err := client.Reboot(ctx); err != nil { + t.Fatalf("Unexpected error rebooting: %v", err) + } + }) + + t.Run("returns_error_when_D-Bus_call_fails", func(t *testing.T) { + t.Parallel() + + expectedError := fmt.Errorf("reboot error") + + connectionWithFailingObjectCall := &dbus.MockConnection{ + ObjectF: func(string, godbus.ObjectPath) godbus.BusObject { + return &dbus.MockObject{ + CallWithContextF: func(ctx context.Context, method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + return &godbus.Call{ + Err: expectedError, + } + }, + } + }, + } + + client, err := login1.New(connectionWithFailingObjectCall) + if err != nil { + t.Fatalf("Unexpected error creating client: %v", err) + } + + if err := client.Reboot(context.Background()); !errors.Is(err, expectedError) { + t.Fatalf("Unexpected error rebooting: %v", err) + } + }) +} diff --git a/pkg/updateengine/client.go b/pkg/updateengine/client.go index 74f194d90..f6c577e26 100644 --- a/pkg/updateengine/client.go +++ b/pkg/updateengine/client.go @@ -16,10 +16,8 @@ package updateengine import ( "fmt" - "os" - "strconv" - dbus "github.com/godbus/dbus/v5" + godbus "github.com/godbus/dbus/v5" ) const ( @@ -42,77 +40,42 @@ type Client interface { // ReceiveStatuses listens for D-Bus signals coming from update_engine and converts them to Statuses // emitted into a given channel. It returns when stop channel gets closed or when the value is sent to it. ReceiveStatuses(rcvr chan<- Status, stop <-chan struct{}) - - // Close closes underlying connection to the DBus broker. It is up to the user to close the connection - // and avoid leaking it. - // - // Receive statuses call must be stopped before closing the connection. - Close() error } -// DBusConnection is set of methods which client expects D-Bus connection to implement. +// DBusConnection is a functionality expected from given D-Bus client. type DBusConnection interface { - Auth([]dbus.Auth) error - Hello() error - Close() error - AddMatchSignal(...dbus.MatchOption) error - Signal(chan<- *dbus.Signal) - Object(string, dbus.ObjectPath) dbus.BusObject + AddMatchSignal(...godbus.MatchOption) error + Signal(chan<- *godbus.Signal) + Object(string, godbus.ObjectPath) godbus.BusObject } -// DBusConnector is a constructor function providing D-Bus connection. -type DBusConnector func() (DBusConnection, error) - -// DBusSystemPrivateConnector is a standard update_engine connector using system bus. -func DBusSystemPrivateConnector() (DBusConnection, error) { - return dbus.SystemBusPrivate() +type caller interface { + Call(method string, flags godbus.Flags, args ...interface{}) *godbus.Call } type client struct { conn DBusConnection - object dbus.BusObject - ch chan *dbus.Signal + object caller } // New creates new instance of Client and initializes it. -func New(newConnection DBusConnector) (Client, error) { - conn, err := newConnection() - if err != nil { - return nil, fmt.Errorf("opening private connection to system bus: %w", err) - } - - methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} - - if err := conn.Auth(methods); err != nil { - // Best effort closing the connection. - _ = conn.Close() - - return nil, fmt.Errorf("authenticating to system bus: %w", err) - } - - if err := conn.Hello(); err != nil { - // Best effort closing the connection. - _ = conn.Close() - - return nil, fmt.Errorf("sending hello to system bus: %w", err) +func New(conn DBusConnection) (Client, error) { + if conn == nil { + return nil, fmt.Errorf("connection can't be nil") } - matchOptions := []dbus.MatchOption{ - dbus.WithMatchInterface(DBusInterface), - dbus.WithMatchMember(DBusSignalNameStatusUpdate), + matchOptions := []godbus.MatchOption{ + godbus.WithMatchInterface(DBusInterface), + godbus.WithMatchMember(DBusSignalNameStatusUpdate), } if err := conn.AddMatchSignal(matchOptions...); err != nil { return nil, fmt.Errorf("adding filter: %w", err) } - ch := make(chan *dbus.Signal, signalBuffer) - conn.Signal(ch) - return &client{ - ch: ch, conn: conn, - object: conn.Object(DBusDestination, dbus.ObjectPath(DBusPath)), + object: conn.Object(DBusDestination, godbus.ObjectPath(DBusPath)), }, nil } @@ -121,6 +84,10 @@ func New(newConnection DBusConnector) (Client, error) { // get the initial status and send it on the rcvr channel before receiving // starts. func (c *client) ReceiveStatuses(rcvr chan<- Status, stop <-chan struct{}) { + ch := make(chan *godbus.Signal, signalBuffer) + + c.conn.Signal(ch) + // If there is an error getting the current status, ignore it and just // move onto the main loop. st, _ := c.getStatus() @@ -130,22 +97,12 @@ func (c *client) ReceiveStatuses(rcvr chan<- Status, stop <-chan struct{}) { select { case <-stop: return - case signal := <-c.ch: + case signal := <-ch: rcvr <- NewStatus(signal.Body) } } } -// Close closes internal D-Bus connection. -func (c *client) Close() error { - if c.conn != nil { - return c.conn.Close() - } - - return nil -} - -// getStatus gets the current status from update_engine. func (c *client) getStatus() (Status, error) { call := c.object.Call(DBusInterface+"."+DBusMethodNameGetStatus, 0) if call.Err != nil { diff --git a/pkg/updateengine/client_integration_test.go b/pkg/updateengine/client_integration_test.go deleted file mode 100644 index 4be81eca3..000000000 --- a/pkg/updateengine/client_integration_test.go +++ /dev/null @@ -1,244 +0,0 @@ -//go:build integration -// +build integration - -package updateengine_test - -import ( - "fmt" - "os" - "strconv" - "testing" - "time" - - dbus "github.com/godbus/dbus/v5" - "github.com/google/go-cmp/cmp" - - "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/updateengine" -) - -//nolint:paralleltest,funlen,cyclop // Test uses environment variables and D-Bus which are global. -// Just many subtests. -func Test_Receiving_status(t *testing.T) { - t.Run("emits_first_status_immediately_after_start", func(t *testing.T) { - ch, _ := testGetStatusReceiver(t, updateengine.Status{}) - - timeout := time.NewTimer(time.Second) - select { - case <-ch: - case <-timeout.C: - t.Fatal("Failed getting status within expected timeframe") - } - }) - - t.Run("parses_received_values_in_order_defined_by_update_engine", func(t *testing.T) { - expectedStatus := testStatus() - - ch, _ := testGetStatusReceiver(t, expectedStatus) - - timeout := time.NewTimer(time.Second) - select { - case status := <-ch: - if diff := cmp.Diff(expectedStatus, status); diff != "" { - t.Fatalf("Unexpectected status values received:\n%s", diff) - } - case <-timeout.C: - t.Fatal("Failed getting status within expected timeframe") - } - }) - - t.Run("forwards_status_updates_received_from_update_engine_to_given_receiver_channel", func(t *testing.T) { - firstExpectedStatus := updateengine.Status{} - - statusCh, conn := testGetStatusReceiver(t, firstExpectedStatus) - - secondExpectedStatus := testStatus() - - lastCheckedTime, progress, currentOperation, newVersion, newSize, _ := statusToRawValues(secondExpectedStatus, nil) - - withMockStatusUpdate(t, conn, lastCheckedTime, progress, currentOperation, newVersion, newSize) - - timeout := time.NewTimer(time.Second) - - select { - case status := <-statusCh: - if diff := cmp.Diff(firstExpectedStatus, status); diff != "" { - t.Fatalf("Unexpectected first status values received (-expected/+got):\n%s", diff) - } - case <-timeout.C: - t.Fatal("Failed getting first status within expected timeframe") - } - - timeout.Reset(time.Second) - - select { - case status := <-statusCh: - if diff := cmp.Diff(secondExpectedStatus, status); diff != "" { - t.Fatalf("Unexpectected second status values received:\n%s", diff) - } - case <-timeout.C: - t.Fatal("Failed getting second status within expected timeframe") - } - }) - - t.Run("returns_empty_status_when_getting_initial_status_fails", func(t *testing.T) { - expectedStatus := updateengine.Status{} - - statusCh, conn := testGetStatusReceiver(t, expectedStatus) - - // Reset method table so GetStatus updates immediately returns error. - // Once https://github.com/flatcar-linux/flatcar-linux-update-operator/issues/100 is solved, - // we can make method call to actually return an error. - tbl := map[string]interface{}{} - if err := conn.ExportMethodTable(tbl, updateengine.DBusPath, updateengine.DBusInterface); err != nil { - t.Fatalf("Failed resetting method table: %v", err) - } - - timeout := time.NewTimer(time.Second) - - select { - case status := <-statusCh: - if diff := cmp.Diff(expectedStatus, status); diff != "" { - t.Fatalf("Unexpectected status values received:\n%s", diff) - } - case <-timeout.C: - t.Fatal("Failed getting status within expected timeframe") - } - }) -} - -func testGetStatusReceiver(t *testing.T, status updateengine.Status) (chan updateengine.Status, *dbus.Conn) { - t.Helper() - - getStatusTestResponse := func(message dbus.Message) (int64, float64, string, string, int64, *dbus.Error) { - return statusToRawValues(status, nil) - } - - conn := withMockGetStatus(t, getStatusTestResponse) - - client, err := updateengine.New(updateengine.DBusSystemPrivateConnector) - if err != nil { - t.Fatalf("Creating client should succeed, got: %v", err) - } - - stop := make(chan struct{}) - - t.Cleanup(func() { - // Stopping receiver routine must be done before closing the client. See - // https://github.com/flatcar-linux/flatcar-linux-update-operator/issues/101 for more details. - close(stop) - if err := client.Close(); err != nil { - t.Fatalf("Failed closing client: %v", err) - } - }) - - ch := make(chan updateengine.Status, 1) - - go client.ReceiveStatuses(ch, stop) - - return ch, conn -} - -const ( - testDbusSocketEnv = "FLUO_TEST_DBUS_SOCKET" -) - -func testStatus() updateengine.Status { - return updateengine.Status{ - LastCheckedTime: 10, - Progress: 20, - CurrentOperation: updateengine.UpdateStatusVerifying, - NewVersion: "1.2.3", - NewSize: 30, - } -} - -func statusToRawValues(s updateengine.Status, err *dbus.Error) (int64, float64, string, string, int64, *dbus.Error) { - return s.LastCheckedTime, s.Progress, s.CurrentOperation, s.NewVersion, s.NewSize, err -} - -func testSystemConnection(t *testing.T) *dbus.Conn { - t.Helper() - - t.Setenv("DBUS_SYSTEM_BUS_ADDRESS", fmt.Sprintf("unix:path=%s", os.Getenv(testDbusSocketEnv))) - - conn, err := dbus.SystemBusPrivate() - if err != nil { - t.Fatalf("Opening private connection to system bus: %v", err) - } - - return conn -} - -func withMockGetStatus(t *testing.T, getStatusF interface{}) *dbus.Conn { - t.Helper() - - conn := testSystemConnection(t) - - methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} - - if err := conn.Auth(methods); err != nil { - t.Fatalf("Failed authenticating to system bus: %v", err) - } - - if err := conn.Hello(); err != nil { - t.Fatalf("Failed sending hello to system bus: %v", err) - } - - if _, err := conn.RequestName(updateengine.DBusDestination, 0); err != nil { - t.Fatalf("Requesting name: %v", err) - } - - t.Cleanup(func() { - if _, err := conn.ReleaseName(updateengine.DBusDestination); err != nil { - t.Fatalf("Failed releasing name: %v", err) - } - }) - - tbl := map[string]interface{}{ - updateengine.DBusMethodNameGetStatus: getStatusF, - } - - if err := conn.ExportMethodTable(tbl, updateengine.DBusPath, updateengine.DBusInterface); err != nil { - t.Fatalf("Exporting method table: %v", err) - } - - t.Cleanup(func() { - tbl := map[string]interface{}{} - if err := conn.ExportMethodTable(tbl, updateengine.DBusPath, updateengine.DBusInterface); err != nil { - t.Fatalf("Failed resetting method table: %v", err) - } - }) - - return conn -} - -func withMockStatusUpdate(t *testing.T, conn *dbus.Conn, values ...interface{}) { - t.Helper() - - emitName := fmt.Sprintf("%s.%s", updateengine.DBusInterface, updateengine.DBusSignalNameStatusUpdate) - - ticker := time.NewTicker(100 * time.Millisecond) - stopCh := make(chan struct{}) - doneCh := make(chan struct{}) - - t.Cleanup(func() { - close(stopCh) - <-doneCh - }) - - go func() { - for { - select { - case <-ticker.C: - if err := conn.Emit(updateengine.DBusPath, emitName, values...); err != nil { - t.Logf("Failed emitting mock status: %v", err) - t.Fail() - } - case <-stopCh: - close(doneCh) - - return - } - } - }() -} diff --git a/pkg/updateengine/client_test.go b/pkg/updateengine/client_test.go index 3875a9b47..8d0069d29 100644 --- a/pkg/updateengine/client_test.go +++ b/pkg/updateengine/client_test.go @@ -3,161 +3,298 @@ package updateengine_test import ( "errors" "fmt" + "reflect" "testing" + "time" + godbus "github.com/godbus/dbus/v5" + "github.com/google/go-cmp/cmp" + + "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/dbus" "github.com/flatcar-linux/flatcar-linux-update-operator/pkg/updateengine" - dbus "github.com/godbus/dbus/v5" ) -//nolint:paralleltest,funlen,tparallel // Test uses environment variables, which are global. -func Test_Creating_client_fails_when(t *testing.T) { - t.Run("connecting_to_system_bus_fails", func(t *testing.T) { - t.Setenv("DBUS_SYSTEM_BUS_ADDRESS", "foo") +//nolint:funlen,cyclop // Just many test cases. +func Test_Receiving_status(t *testing.T) { + t.Parallel() + + t.Run("emits_first_status_immediately_after_start", func(t *testing.T) { + t.Parallel() + + mockConnection := &dbus.MockConnection{ + ObjectF: func(string, godbus.ObjectPath) godbus.BusObject { + return &dbus.MockObject{ + CallF: func(method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + return &godbus.Call{ + Body: statusToSignalBody(updateengine.Status{}), + } + }, + } + }, + } - if _, err := updateengine.New(updateengine.DBusSystemPrivateConnector); err == nil { - t.Fatalf("Creating client should fail when unable to connect to system bus") + client, err := updateengine.New(mockConnection) + if err != nil { + t.Fatalf("Got unexpected error while creating client: %v", err) } - }) - t.Run("D-Bus_authentication_fails", func(t *testing.T) { - t.Parallel() + stop := make(chan struct{}) - expectedError := fmt.Errorf("auth failed") + t.Cleanup(func() { + close(stop) + }) - closeCalled := false + statusCh := make(chan updateengine.Status, 1) - connector := newMockDBusConnection() - connector.authF = func([]dbus.Auth) error { return expectedError } - connector.closeF = func() error { - closeCalled = true + go client.ReceiveStatuses(statusCh, stop) - return fmt.Errorf("closing error") + timeout := time.NewTimer(time.Second) + select { + case <-statusCh: + case <-timeout.C: + t.Fatal("Failed getting status within expected timeframe") } + }) + + t.Run("parses_received_values_in_order_defined_by_update_engine", func(t *testing.T) { + t.Parallel() - client, err := updateengine.New(func() (updateengine.DBusConnection, error) { return connector, nil }) - if !errors.Is(err, expectedError) { - t.Fatalf("Got unexpected error while creating client, expected %q, got %q", expectedError, err) + expectedStatus := testStatus() + + mockConnection := &dbus.MockConnection{ + ObjectF: func(string, godbus.ObjectPath) godbus.BusObject { + return &dbus.MockObject{ + CallF: func(method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + return &godbus.Call{ + Body: statusToSignalBody(expectedStatus), + } + }, + } + }, } - if client != nil { - t.Fatalf("Expected client to be nil when creating fails") + client, err := updateengine.New(mockConnection) + if err != nil { + t.Fatalf("Got unexpected error while creating client: %v", err) } - t.Run("and_tries_to_close_the_client_while_ignoring_closing_error", func(t *testing.T) { - if !closeCalled { - t.Fatalf("Expected close function to be called") - } + stop := make(chan struct{}) + + t.Cleanup(func() { + close(stop) }) - }) - t.Run("D-Bus_hello_fails", func(t *testing.T) { - t.Parallel() + statusCh := make(chan updateengine.Status, 1) - expectedError := fmt.Errorf("hello failed") + go client.ReceiveStatuses(statusCh, stop) - closeCalled := false + timeout := time.NewTimer(time.Second) + select { + case status := <-statusCh: + if diff := cmp.Diff(expectedStatus, status); diff != "" { + t.Fatalf("Unexpectected status values received:\n%s", diff) + } + case <-timeout.C: + t.Fatal("Failed getting status within expected timeframe") + } + }) - connector := newMockDBusConnection() - connector.helloF = func() error { return expectedError } - connector.closeF = func() error { - closeCalled = true + t.Run("forwards_status_updates_received_from_update_engine_to_given_receiver_channel", func(t *testing.T) { + t.Parallel() - return fmt.Errorf("closing error") + firstExpectedStatus := updateengine.Status{} + + secondExpectedStatus := testStatus() + + mockConnection := &dbus.MockConnection{ + ObjectF: func(string, godbus.ObjectPath) godbus.BusObject { + return &dbus.MockObject{ + CallF: func(method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + return &godbus.Call{ + Body: statusToSignalBody(firstExpectedStatus), + } + }, + } + }, + SignalF: func(ch chan<- *godbus.Signal) { + ch <- &godbus.Signal{ + Body: statusToSignalBody(secondExpectedStatus), + } + }, } - client, err := updateengine.New(func() (updateengine.DBusConnection, error) { return connector, nil }) - if !errors.Is(err, expectedError) { - t.Fatalf("Got unexpected error while creating client, expected %q, got %q", expectedError, err) + client, err := updateengine.New(mockConnection) + if err != nil { + t.Fatalf("Got unexpected error while creating client: %v", err) } - if client != nil { - t.Fatalf("Expected client to be nil when creating fails") + stop := make(chan struct{}) + + t.Cleanup(func() { + close(stop) + }) + + statusCh := make(chan updateengine.Status, 1) + + go client.ReceiveStatuses(statusCh, stop) + + timeout := time.NewTimer(time.Second) + + select { + case status := <-statusCh: + if diff := cmp.Diff(firstExpectedStatus, status); diff != "" { + t.Fatalf("Unexpectected first status values received (-expected/+got):\n%s", diff) + } + case <-timeout.C: + t.Fatal("Failed getting first status within expected timeframe") } - t.Run("and_tries_to_close_the_client_while_ignoring_closing_error", func(t *testing.T) { - if !closeCalled { - t.Fatalf("Expected close function to be called") + timeout.Reset(time.Second) + + select { + case status := <-statusCh: + if diff := cmp.Diff(secondExpectedStatus, status); diff != "" { + t.Fatalf("Unexpectected second status values received:\n%s", diff) } - }) + case <-timeout.C: + t.Fatal("Failed getting second status within expected timeframe") + } }) - t.Run("adding_D-Bus_filter_fails", func(t *testing.T) { + t.Run("returns_empty_status_when_getting_initial_status_fails", func(t *testing.T) { t.Parallel() - expectedError := fmt.Errorf("match signal failed") - - connector := newMockDBusConnection() - connector.addMatchSignalF = func(...dbus.MatchOption) error { return expectedError } + expectedStatus := updateengine.Status{} + + mockConnection := &dbus.MockConnection{ + ObjectF: func(string, godbus.ObjectPath) godbus.BusObject { + return &dbus.MockObject{ + CallF: func(method string, flags godbus.Flags, args ...interface{}) *godbus.Call { + return &godbus.Call{ + Body: statusToSignalBody(testStatus()), + Err: fmt.Errorf("some error"), + } + }, + } + }, + } - client, err := updateengine.New(func() (updateengine.DBusConnection, error) { return connector, nil }) - if !errors.Is(err, expectedError) { - t.Fatalf("Got unexpected error while creating client, expected %q, got %q", expectedError, err) + client, err := updateengine.New(mockConnection) + if err != nil { + t.Fatalf("Got unexpected error while creating client: %v", err) } - if client != nil { - t.Fatalf("Expected client to be nil when creating fails") + stop := make(chan struct{}) + + t.Cleanup(func() { + close(stop) + }) + + statusCh := make(chan updateengine.Status, 1) + + go client.ReceiveStatuses(statusCh, stop) + + timeout := time.NewTimer(time.Second) + + select { + case status := <-statusCh: + if diff := cmp.Diff(expectedStatus, status); diff != "" { + t.Fatalf("Unexpectected status values received:\n%s", diff) + } + case <-timeout.C: + t.Fatal("Failed getting status within expected timeframe") } }) } -func Test_Closing_client_returns_error_when_closing_DBus_client_fails(t *testing.T) { +//nolint:funlen,gocognit,cyclop // Just many test cases. +func Test_Creating_client(t *testing.T) { t.Parallel() - expectedError := fmt.Errorf("closing failed") + t.Run("subscribes_to_status_update_signals", func(t *testing.T) { + t.Parallel() - connector := newMockDBusConnection() - connector.closeF = func() error { return expectedError } + matchOptionsCheckingConnection := &dbus.MockConnection{ + AddMatchSignalF: func(matchOptions ...godbus.MatchOption) error { + foundInterface := false + foundMember := false + + for _, option := range matchOptions { + optionValue := reflect.ValueOf(&option).Elem() + key := optionValue.Field(0) + value := optionValue.Field(1) + + switch key.String() { + case "interface": + if value.String() == updateengine.DBusInterface { + foundInterface = true + } + case "member": + if value.String() == updateengine.DBusSignalNameStatusUpdate { + foundMember = true + } + } + } + + if !foundInterface { + t.Fatal("Did not receive match option with interface specified") + } + + if !foundMember { + t.Fatal("Did not receive match option with member specified") + } + + return nil + }, + } - client, err := updateengine.New(func() (updateengine.DBusConnection, error) { return connector, nil }) - if err != nil { - t.Fatalf("Unexpected error creating client: %v", err) - } + if _, err := updateengine.New(matchOptionsCheckingConnection); err != nil { + t.Fatalf("Got unexpected error while creating client: %v", err) + } + }) - if err := client.Close(); !errors.Is(err, expectedError) { - t.Fatalf("Got unexpected error closing the client, expected %q, got %q", expectedError, err) - } -} + t.Run("fails_when", func(t *testing.T) { + t.Parallel() -type mockDBusConnection struct { - authF func([]dbus.Auth) error - helloF func() error - closeF func() error - addMatchSignalF func(...dbus.MatchOption) error - signalF func(chan<- *dbus.Signal) - objectF func(string, dbus.ObjectPath) dbus.BusObject -} + t.Run("no_D-Bus_connection_is_given", func(t *testing.T) { + t.Parallel() -func (m *mockDBusConnection) Auth(methods []dbus.Auth) error { - return m.authF(methods) -} + if _, err := updateengine.New(nil); err == nil { + t.Fatalf("Creating client should fail when unable to connect to system bus") + } + }) -func (m *mockDBusConnection) Hello() error { - return m.helloF() -} + t.Run("adding_D-Bus_filter_fails", func(t *testing.T) { + t.Parallel() -func (m *mockDBusConnection) Close() error { - return m.closeF() -} + expectedError := fmt.Errorf("match signal failed") -func (m *mockDBusConnection) AddMatchSignal(options ...dbus.MatchOption) error { - return m.addMatchSignalF(options...) -} + mockConnection := &dbus.MockConnection{ + AddMatchSignalF: func(...godbus.MatchOption) error { return expectedError }, + } -func (m *mockDBusConnection) Signal(ch chan<- *dbus.Signal) { - m.signalF(ch) -} + client, err := updateengine.New(mockConnection) + if !errors.Is(err, expectedError) { + t.Fatalf("Got unexpected error while creating client, expected %q, got %q", expectedError, err) + } -func (m *mockDBusConnection) Object(dest string, path dbus.ObjectPath) dbus.BusObject { - return m.objectF(dest, path) + if client != nil { + t.Fatalf("Expected client to be nil when creating fails") + } + }) + }) } -func newMockDBusConnection() *mockDBusConnection { - return &mockDBusConnection{ - authF: func([]dbus.Auth) error { return nil }, - helloF: func() error { return nil }, - closeF: func() error { return nil }, - addMatchSignalF: func(...dbus.MatchOption) error { return nil }, - signalF: func(chan<- *dbus.Signal) {}, - objectF: func(string, dbus.ObjectPath) dbus.BusObject { return &dbus.Object{} }, +func testStatus() updateengine.Status { + return updateengine.Status{ + LastCheckedTime: 10, + Progress: 20, + CurrentOperation: updateengine.UpdateStatusVerifying, + NewVersion: "1.2.3", + NewSize: 30, } } + +func statusToSignalBody(s updateengine.Status) []interface{} { + return []interface{}{s.LastCheckedTime, s.Progress, s.CurrentOperation, s.NewVersion, s.NewSize} +} diff --git a/vendor/github.com/coreos/go-systemd/v22/LICENSE b/vendor/github.com/coreos/go-systemd/v22/LICENSE deleted file mode 100644 index 37ec93a14..000000000 --- a/vendor/github.com/coreos/go-systemd/v22/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/vendor/github.com/coreos/go-systemd/v22/NOTICE b/vendor/github.com/coreos/go-systemd/v22/NOTICE deleted file mode 100644 index 23a0ada2f..000000000 --- a/vendor/github.com/coreos/go-systemd/v22/NOTICE +++ /dev/null @@ -1,5 +0,0 @@ -CoreOS Project -Copyright 2018 CoreOS, Inc - -This product includes software developed at CoreOS, Inc. -(http://www.coreos.com/). diff --git a/vendor/github.com/coreos/go-systemd/v22/login1/dbus.go b/vendor/github.com/coreos/go-systemd/v22/login1/dbus.go deleted file mode 100644 index ca71308c0..000000000 --- a/vendor/github.com/coreos/go-systemd/v22/login1/dbus.go +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright 2015 CoreOS, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package login1 provides integration with the systemd logind API. See http://www.freedesktop.org/wiki/Software/systemd/logind/ -package login1 - -import ( - "fmt" - "os" - "strconv" - "strings" - - "github.com/godbus/dbus/v5" -) - -const ( - dbusDest = "org.freedesktop.login1" - dbusInterface = "org.freedesktop.login1.Manager" - dbusPath = "/org/freedesktop/login1" -) - -// Conn is a connection to systemds dbus endpoint. -type Conn struct { - conn *dbus.Conn - object dbus.BusObject -} - -// New establishes a connection to the system bus and authenticates. -func New() (*Conn, error) { - c := new(Conn) - - if err := c.initConnection(); err != nil { - return nil, err - } - - return c, nil -} - -// Close closes the dbus connection -func (c *Conn) Close() { - if c == nil { - return - } - - if c.conn != nil { - c.conn.Close() - } -} - -func (c *Conn) initConnection() error { - var err error - c.conn, err = dbus.SystemBusPrivate() - if err != nil { - return err - } - - // Only use EXTERNAL method, and hardcode the uid (not username) - // to avoid a username lookup (which requires a dynamically linked - // libc) - methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} - - err = c.conn.Auth(methods) - if err != nil { - c.conn.Close() - return err - } - - err = c.conn.Hello() - if err != nil { - c.conn.Close() - return err - } - - c.object = c.conn.Object("org.freedesktop.login1", dbus.ObjectPath(dbusPath)) - - return nil -} - -// Session object definition. -type Session struct { - ID string - UID uint32 - User string - Seat string - Path dbus.ObjectPath -} - -// User object definition. -type User struct { - UID uint32 - Name string - Path dbus.ObjectPath -} - -func (s Session) toInterface() []interface{} { - return []interface{}{s.ID, s.UID, s.User, s.Seat, s.Path} -} - -func sessionFromInterfaces(session []interface{}) (*Session, error) { - if len(session) < 5 { - return nil, fmt.Errorf("invalid number of session fields: %d", len(session)) - } - id, ok := session[0].(string) - if !ok { - return nil, fmt.Errorf("failed to typecast session field 0 to string") - } - uid, ok := session[1].(uint32) - if !ok { - return nil, fmt.Errorf("failed to typecast session field 1 to uint32") - } - user, ok := session[2].(string) - if !ok { - return nil, fmt.Errorf("failed to typecast session field 2 to string") - } - seat, ok := session[3].(string) - if !ok { - return nil, fmt.Errorf("failed to typecast session field 2 to string") - } - path, ok := session[4].(dbus.ObjectPath) - if !ok { - return nil, fmt.Errorf("failed to typecast session field 4 to ObjectPath") - } - - ret := Session{ID: id, UID: uid, User: user, Seat: seat, Path: path} - return &ret, nil -} - -func userFromInterfaces(user []interface{}) (*User, error) { - if len(user) < 3 { - return nil, fmt.Errorf("invalid number of user fields: %d", len(user)) - } - uid, ok := user[0].(uint32) - if !ok { - return nil, fmt.Errorf("failed to typecast user field 0 to uint32") - } - name, ok := user[1].(string) - if !ok { - return nil, fmt.Errorf("failed to typecast session field 1 to string") - } - path, ok := user[2].(dbus.ObjectPath) - if !ok { - return nil, fmt.Errorf("failed to typecast user field 2 to ObjectPath") - } - - ret := User{UID: uid, Name: name, Path: path} - return &ret, nil -} - -// GetActiveSession may be used to get the session object path for the current active session -func (c *Conn) GetActiveSession() (dbus.ObjectPath, error) { - var seat0Path dbus.ObjectPath - if err := c.object.Call(dbusInterface+".GetSeat", 0, "seat0").Store(&seat0Path); err != nil { - return "", err - } - - seat0Obj := c.conn.Object(dbusDest, seat0Path) - activeSession, err := seat0Obj.GetProperty(dbusDest + ".Seat.ActiveSession") - if err != nil { - return "", err - } - activeSessionMap, ok := activeSession.Value().([]interface{}) - if !ok || len(activeSessionMap) < 2 { - return "", fmt.Errorf("failed to typecast active session map") - } - - activeSessionPath, ok := activeSessionMap[1].(dbus.ObjectPath) - if !ok { - return "", fmt.Errorf("failed to typecast dbus active session Path") - } - return activeSessionPath, nil -} - -// GetSessionUser may be used to get the user of specific session -func (c *Conn) GetSessionUser(sessionPath dbus.ObjectPath) (*User, error) { - if len(sessionPath) == 0 { - return nil, fmt.Errorf("empty sessionPath") - } - - activeSessionObj := c.conn.Object(dbusDest, sessionPath) - sessionUserName, err := activeSessionObj.GetProperty(dbusDest + ".Session.Name") - if err != nil { - return nil, err - } - - sessionUser, err := activeSessionObj.GetProperty(dbusDest + ".Session.User") - if err != nil { - return nil, err - } - dbusUser, ok := sessionUser.Value().([]interface{}) - if !ok { - return nil, fmt.Errorf("failed to typecast dbus session user") - } - - if len(dbusUser) < 2 { - return nil, fmt.Errorf("invalid number of user fields: %d", len(dbusUser)) - } - uid, ok := dbusUser[0].(uint32) - if !ok { - return nil, fmt.Errorf("failed to typecast user field 0 to uint32") - } - path, ok := dbusUser[1].(dbus.ObjectPath) - if !ok { - return nil, fmt.Errorf("failed to typecast user field 1 to ObjectPath") - } - - user := User{UID: uid, Name: strings.Trim(sessionUserName.String(), "\""), Path: path} - - return &user, nil -} - -// GetSessionDisplay may be used to get the display for specific session -func (c *Conn) GetSessionDisplay(sessionPath dbus.ObjectPath) (string, error) { - if len(sessionPath) == 0 { - return "", fmt.Errorf("empty sessionPath") - } - sessionObj := c.conn.Object(dbusDest, sessionPath) - display, err := sessionObj.GetProperty(dbusDest + ".Session.Display") - if err != nil { - return "", err - } - - return strings.Trim(display.String(), "\""), nil -} - -// GetSession may be used to get the session object path for the session with the specified ID. -func (c *Conn) GetSession(id string) (dbus.ObjectPath, error) { - var out interface{} - if err := c.object.Call(dbusInterface+".GetSession", 0, id).Store(&out); err != nil { - return "", err - } - - ret, ok := out.(dbus.ObjectPath) - if !ok { - return "", fmt.Errorf("failed to typecast session to ObjectPath") - } - - return ret, nil -} - -// ListSessions returns an array with all current sessions. -func (c *Conn) ListSessions() ([]Session, error) { - out := [][]interface{}{} - if err := c.object.Call(dbusInterface+".ListSessions", 0).Store(&out); err != nil { - return nil, err - } - - ret := []Session{} - for _, el := range out { - session, err := sessionFromInterfaces(el) - if err != nil { - return nil, err - } - ret = append(ret, *session) - } - return ret, nil -} - -// ListUsers returns an array with all currently logged in users. -func (c *Conn) ListUsers() ([]User, error) { - out := [][]interface{}{} - if err := c.object.Call(dbusInterface+".ListUsers", 0).Store(&out); err != nil { - return nil, err - } - - ret := []User{} - for _, el := range out { - user, err := userFromInterfaces(el) - if err != nil { - return nil, err - } - ret = append(ret, *user) - } - return ret, nil -} - -// LockSession asks the session with the specified ID to activate the screen lock. -func (c *Conn) LockSession(id string) { - c.object.Call(dbusInterface+".LockSession", 0, id) -} - -// LockSessions asks all sessions to activate the screen locks. This may be used to lock any access to the machine in one action. -func (c *Conn) LockSessions() { - c.object.Call(dbusInterface+".LockSessions", 0) -} - -// TerminateSession forcibly terminate one specific session. -func (c *Conn) TerminateSession(id string) { - c.object.Call(dbusInterface+".TerminateSession", 0, id) -} - -// TerminateUser forcibly terminates all processes of a user. -func (c *Conn) TerminateUser(uid uint32) { - c.object.Call(dbusInterface+".TerminateUser", 0, uid) -} - -// Reboot asks logind for a reboot optionally asking for auth. -func (c *Conn) Reboot(askForAuth bool) { - c.object.Call(dbusInterface+".Reboot", 0, askForAuth) -} - -// Inhibit takes inhibition lock in logind. -func (c *Conn) Inhibit(what, who, why, mode string) (*os.File, error) { - var fd dbus.UnixFD - - err := c.object.Call(dbusInterface+".Inhibit", 0, what, who, why, mode).Store(&fd) - if err != nil { - return nil, err - } - - return os.NewFile(uintptr(fd), "inhibit"), nil -} - -// Subscribe to signals on the logind dbus -func (c *Conn) Subscribe(members ...string) chan *dbus.Signal { - for _, member := range members { - c.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, - fmt.Sprintf("type='signal',interface='org.freedesktop.login1.Manager',member='%s'", member)) - } - ch := make(chan *dbus.Signal, 10) - c.conn.Signal(ch) - return ch -} - -// PowerOff asks logind for a power off optionally asking for auth. -func (c *Conn) PowerOff(askForAuth bool) { - c.object.Call(dbusInterface+".PowerOff", 0, askForAuth) -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 5ca17071a..8ae78ec11 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,9 +1,6 @@ # github.com/blang/semver/v4 v4.0.0 ## explicit; go 1.14 github.com/blang/semver/v4 -# github.com/coreos/go-systemd/v22 v22.3.2 -## explicit; go 1.12 -github.com/coreos/go-systemd/v22/login1 # github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f ## explicit github.com/coreos/pkg/flagutil