talos/internal/app/machined/pkg/system/service_runner_test.go
Dmitriy Matrenichev dad9c40c73
chore: simplify code
- replace `interface{}` with `any` using `gofmt -r 'interface{} -> any -w'`
- replace `a = []T{}` with `var a []T` where possible.
- replace `a = []T{}` with `a = make([]T, 0, len(b))` where possible.

Signed-off-by: Dmitriy Matrenichev <dmitry.matrenichev@siderolabs.com>
2024-07-08 18:14:00 +03:00

468 lines
11 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package system_test
import (
"errors"
"testing"
"time"
"github.com/siderolabs/go-retry/retry"
"github.com/stretchr/testify/suite"
"github.com/siderolabs/talos/internal/app/machined/pkg/system"
"github.com/siderolabs/talos/internal/app/machined/pkg/system/events"
"github.com/siderolabs/talos/pkg/conditions"
)
type ServiceRunnerSuite struct {
suite.Suite
}
func (suite *ServiceRunnerSuite) assertStateSequence(expectedStates []events.ServiceState, sr *system.ServiceRunner) {
states := make([]events.ServiceState, 0, 1000)
for _, event := range sr.GetEventHistory(1000) {
states = append(states, event.State)
}
suite.Assert().Equal(expectedStates, states)
}
func (suite *ServiceRunnerSuite) TestFullFlow() {
sr := system.NewServiceRunner(system.Services(nil), &MockService{
condition: conditions.None(),
}, nil)
errCh := make(chan error)
go func() {
errCh <- sr.Run()
}()
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
state := sr.AsProto().State
if state != events.StateRunning.String() {
return retry.ExpectedErrorf("service should be running")
}
return nil
}))
select {
case <-errCh:
suite.Require().Fail("service running should be still running")
default:
}
sr.Shutdown()
suite.Assert().NoError(<-errCh)
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StateWaiting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
}, sr)
protoService := sr.AsProto()
suite.Assert().Equal("MockRunner", protoService.Id)
suite.Assert().Equal("Running", protoService.State)
suite.Assert().True(protoService.Health.Unknown)
suite.Assert().Len(protoService.Events.Events, 5)
}
func (suite *ServiceRunnerSuite) TestFullFlowHealthy() {
sr := system.NewServiceRunner(system.Services(nil), &MockHealthcheckedService{}, nil)
errCh := make(chan error)
go func() {
errCh <- sr.Run()
}()
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
health := sr.AsProto().Health
if health.Unknown || !health.Healthy {
return retry.ExpectedErrorf("service should be healthy")
}
return nil
}))
select {
case <-errCh:
suite.Require().Fail("service running should be still running")
default:
}
sr.Shutdown()
suite.Assert().NoError(<-errCh)
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
events.StateRunning, // one more notification when service is healthy
}, sr)
}
func (suite *ServiceRunnerSuite) TestFullFlowHealthChanges() {
m := MockHealthcheckedService{
MockService: MockService{
condition: conditions.None(),
},
}
sr := system.NewServiceRunner(system.Services(nil), &m, nil)
errCh := make(chan error)
go func() {
errCh <- sr.Run()
}()
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
health := sr.AsProto().Health
if health.Unknown || !health.Healthy {
return retry.ExpectedErrorf("service should be healthy")
}
return nil
}))
m.SetHealthy(false)
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
health := sr.AsProto().Health
if health.Unknown || health.Healthy {
return retry.ExpectedErrorf("service should be not healthy")
}
return nil
}))
m.SetHealthy(true)
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
health := sr.AsProto().Health
if health.Unknown || !health.Healthy {
return retry.ExpectedErrorf("service should be healthy")
}
return nil
}))
sr.Shutdown()
suite.Assert().NoError(<-errCh)
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StateWaiting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
events.StateRunning, // initial: healthy
events.StateRunning, // not healthy
events.StateRunning, // once again healthy
}, sr)
}
func (suite *ServiceRunnerSuite) TestWaitingDescriptionChange() {
oldWaitConditionCheckInterval := system.WaitConditionCheckInterval
system.WaitConditionCheckInterval = 10 * time.Millisecond
defer func() {
system.WaitConditionCheckInterval = oldWaitConditionCheckInterval
}()
cond1 := NewMockCondition("cond1")
cond2 := NewMockCondition("cond2")
sr := system.NewServiceRunner(system.Services(nil), &MockService{
condition: conditions.WaitForAll(cond1, cond2),
}, nil)
errCh := make(chan error)
go func() {
errCh <- sr.Run()
}()
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
state := sr.AsProto().State
if state != events.StateWaiting.String() {
return retry.ExpectedErrorf("service should be waiting")
}
return nil
}))
select {
case <-errCh:
suite.Require().Fail("service running should be still running")
default:
}
close(cond1.done)
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
events := sr.AsProto().Events.Events
lastMsg := events[len(events)-1].Msg
if lastMsg != "Waiting for cond2" {
return retry.ExpectedErrorf("service should be waiting on 2nd condition")
}
return nil
}))
select {
case <-errCh:
suite.Require().Fail("service running should be still running")
default:
}
close(cond2.done)
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
state := sr.AsProto().State
if state != events.StateRunning.String() {
return retry.ExpectedErrorf("service should be running")
}
return nil
}))
sr.Shutdown()
suite.Assert().NoError(<-errCh)
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StateWaiting,
events.StateWaiting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
}, sr)
events := sr.GetEventHistory(10000)
suite.Assert().Equal("Waiting for cond1, cond2", events[1].Message)
suite.Assert().Equal("Waiting for cond2", events[2].Message)
}
func (suite *ServiceRunnerSuite) TestPreStageFail() {
svc := &MockService{
preError: errors.New("pre failed"),
}
sr := system.NewServiceRunner(system.Services(nil), svc, nil)
err := sr.Run()
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StatePreparing,
}, sr)
suite.Assert().EqualError(err, "failed to run pre stage: pre failed")
}
func (suite *ServiceRunnerSuite) TestRunnerStageFail() {
svc := &MockService{
runnerError: errors.New("runner failed"),
}
sr := system.NewServiceRunner(system.Services(nil), svc, nil)
err := sr.Run()
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StatePreparing,
events.StatePreparing,
}, sr)
suite.Assert().EqualError(err, "failed to create runner: runner failed")
}
func (suite *ServiceRunnerSuite) TestRunnerStageSkipped() {
svc := &MockService{
nilRunner: true,
}
sr := system.NewServiceRunner(system.Services(nil), svc, nil)
err := sr.Run()
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StatePreparing,
events.StatePreparing,
}, sr)
suite.Assert().ErrorIs(err, system.ErrSkip)
}
func (suite *ServiceRunnerSuite) TestAbortOnCondition() {
svc := &MockService{
condition: conditions.WaitForFileToExist("/doesntexistever"),
}
sr := system.NewServiceRunner(system.Services(nil), svc, nil)
errCh := make(chan error)
go func() {
errCh <- sr.Run()
}()
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
state := sr.AsProto().State
if state != events.StateWaiting.String() {
return retry.ExpectedErrorf("service should be waiting")
}
return nil
}))
select {
case <-errCh:
suite.Require().Fail("service running should be still running")
default:
}
sr.Shutdown()
suite.Assert().EqualError(<-errCh, "condition failed: context canceled")
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StateWaiting,
}, sr)
}
func (suite *ServiceRunnerSuite) TestPostStateFail() {
svc := &MockService{
condition: conditions.None(),
postError: errors.New("post failed"),
}
sr := system.NewServiceRunner(system.Services(nil), svc, nil)
errCh := make(chan error)
runNotify := make(chan struct{})
go func() {
errCh <- sr.Run(runNotify)
}()
<-runNotify
sr.Shutdown()
suite.Assert().NoError(<-errCh)
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StateWaiting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
events.StateFailed,
}, sr)
}
func (suite *ServiceRunnerSuite) TestRunFail() {
runner := &MockRunner{exitCh: make(chan error)}
svc := &MockService{runner: runner}
sr := system.NewServiceRunner(system.Services(nil), svc, nil)
errCh := make(chan error)
go func() {
errCh <- sr.Run()
}()
runner.exitCh <- errors.New("run failed")
suite.Assert().EqualError(<-errCh, "failed running service: error running service: run failed")
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
}, sr)
}
func (suite *ServiceRunnerSuite) TestFullFlowRestart() {
sr := system.NewServiceRunner(system.Services(nil), &MockService{
condition: conditions.None(),
}, nil)
errCh := make(chan error)
go func() {
errCh <- sr.Run()
}()
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
state := sr.AsProto().State
if state != events.StateRunning.String() {
return retry.ExpectedErrorf("service should be running")
}
return nil
}))
select {
case <-errCh:
suite.Require().Fail("service running should be still running")
default:
}
sr.Shutdown()
suite.Assert().NoError(<-errCh)
notifyCh := make(chan struct{})
go func() {
errCh <- sr.Run(notifyCh)
}()
<-notifyCh
suite.Require().NoError(retry.Constant(time.Minute, retry.WithUnits(10*time.Millisecond)).Retry(func() error {
state := sr.AsProto().State
if state != events.StateRunning.String() {
return retry.ExpectedErrorf("service should be running")
}
return nil
}))
select {
case <-errCh:
suite.Require().Fail("service running should be still running")
default:
}
sr.Shutdown()
suite.Assert().NoError(<-errCh)
suite.assertStateSequence([]events.ServiceState{
events.StateStarting,
events.StateWaiting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
events.StateStarting,
events.StateWaiting,
events.StatePreparing,
events.StatePreparing,
events.StateRunning,
}, sr)
}
func TestServiceRunnerSuite(t *testing.T) {
suite.Run(t, new(ServiceRunnerSuite))
}