Skip to content

Commit

Permalink
feat: implement network.Status resource and controller
Browse files Browse the repository at this point in the history
This resource holds aggregated network status which can be easily used
in various places to wait for the network to reach some desired state.

The state checks are simple right now, we might improve the logic to
make sure all the configured network subsystems reached defined state,
but this might come later as we refine the logic (e.g. to make sure that
all static configuration got applied, etc.)

Signed-off-by: Andrey Smirnov <[email protected]>
  • Loading branch information
smira authored and talos-bot committed Jun 9, 2021
1 parent da329f0 commit 02bd657
Show file tree
Hide file tree
Showing 8 changed files with 526 additions and 0 deletions.
139 changes: 139 additions & 0 deletions internal/app/machined/pkg/controllers/network/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// 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 network

import (
"context"
"fmt"

"github.com/AlekSi/pointer"
"github.com/cosi-project/runtime/pkg/controller"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"go.uber.org/zap"

"github.com/talos-systems/talos/pkg/resources/files"
"github.com/talos-systems/talos/pkg/resources/network"
)

// StatusController manages secrets.Etcd based on configuration.
type StatusController struct{}

// Name implements controller.Controller interface.
func (ctrl *StatusController) Name() string {
return "network.StatusController"
}

// Inputs implements controller.Controller interface.
func (ctrl *StatusController) Inputs() []controller.Input {
return []controller.Input{
{
Namespace: network.NamespaceName,
Type: network.NodeAddressType,
ID: pointer.ToString(network.NodeAddressCurrentID),
Kind: controller.InputWeak,
},
{
Namespace: network.NamespaceName,
Type: network.RouteStatusType,
Kind: controller.InputWeak,
},
{
Namespace: network.NamespaceName,
Type: network.HostnameStatusType,
Kind: controller.InputWeak,
},
{
Namespace: files.NamespaceName,
Type: files.EtcFileStatusType,
Kind: controller.InputWeak,
},
}
}

// Outputs implements controller.Controller interface.
func (ctrl *StatusController) Outputs() []controller.Output {
return []controller.Output{
{
Type: network.StatusType,
Kind: controller.OutputExclusive,
},
}
}

// Run implements controller.Controller interface.
//
//nolint:gocyclo
func (ctrl *StatusController) Run(ctx context.Context, r controller.Runtime, logger *zap.Logger) error {
for {
select {
case <-ctx.Done():
return nil
case <-r.EventCh():
}

result := network.StatusSpec{}

// addresses
currentAddresses, err := r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.NodeAddressType, network.NodeAddressCurrentID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting resource: %w", err)
}
} else {
result.AddressReady = len(currentAddresses.(*network.NodeAddress).TypedSpec().Addresses) > 0
}

// connectivity
list, err := r.List(ctx, resource.NewMetadata(network.NamespaceName, network.RouteStatusType, "", resource.VersionUndefined))
if err != nil {
return fmt.Errorf("error getting routes: %w", err)
}

for _, item := range list.Items {
if item.(*network.RouteStatus).TypedSpec().Destination.IsZero() {
result.ConnectivityReady = true

break
}
}

// hostname
_, err = r.Get(ctx, resource.NewMetadata(network.NamespaceName, network.HostnameStatusType, network.HostnameID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting resource: %w", err)
}
} else {
result.HostnameReady = true
}

// etc files
result.EtcFilesReady = true

for _, requiredFile := range []string{"hosts", "resolv.conf"} {
_, err = r.Get(ctx, resource.NewMetadata(files.NamespaceName, files.EtcFileStatusType, requiredFile, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
return fmt.Errorf("error getting resource: %w", err)
}

result.EtcFilesReady = false

break
}
}

// update output status
if err = r.Modify(ctx, network.NewStatus(network.NamespaceName, network.StatusID),
func(r resource.Resource) error {
*r.(*network.Status).TypedSpec() = result

return nil
}); err != nil {
return fmt.Errorf("error modifying output status: %w", err)
}
}
}
142 changes: 142 additions & 0 deletions internal/app/machined/pkg/controllers/network/status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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/.

//nolint:dupl
package network_test

import (
"context"
"log"
"sync"
"testing"
"time"

"github.com/cosi-project/runtime/pkg/controller/runtime"
"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
"github.com/cosi-project/runtime/pkg/state/impl/inmem"
"github.com/cosi-project/runtime/pkg/state/impl/namespaced"
"github.com/stretchr/testify/suite"
"github.com/talos-systems/go-retry/retry"
"inet.af/netaddr"

netctrl "github.com/talos-systems/talos/internal/app/machined/pkg/controllers/network"
"github.com/talos-systems/talos/pkg/logging"
"github.com/talos-systems/talos/pkg/resources/files"
"github.com/talos-systems/talos/pkg/resources/network"
)

type StatusSuite struct {
suite.Suite

state state.State

runtime *runtime.Runtime
wg sync.WaitGroup

ctx context.Context
ctxCancel context.CancelFunc
}

func (suite *StatusSuite) SetupTest() {
suite.ctx, suite.ctxCancel = context.WithTimeout(context.Background(), 3*time.Minute)

suite.state = state.WrapCore(namespaced.NewState(inmem.Build))

var err error

suite.runtime, err = runtime.NewRuntime(suite.state, logging.Wrap(log.Writer()))
suite.Require().NoError(err)

suite.Require().NoError(suite.runtime.RegisterController(&netctrl.StatusController{}))

suite.startRuntime()
}

func (suite *StatusSuite) startRuntime() {
suite.wg.Add(1)

go func() {
defer suite.wg.Done()

suite.Assert().NoError(suite.runtime.Run(suite.ctx))
}()
}

func (suite *StatusSuite) assertStatus(expected network.StatusSpec) error {
status, err := suite.state.Get(suite.ctx, resource.NewMetadata(network.NamespaceName, network.StatusType, network.StatusID, resource.VersionUndefined))
if err != nil {
if !state.IsNotFoundError(err) {
suite.Require().NoError(err)
}

return retry.ExpectedError(err)
}

if *status.(*network.Status).TypedSpec() != expected {
return retry.ExpectedErrorf("status %+v != expected %+v", *status.(*network.Status).TypedSpec(), expected)
}

return nil
}

func (suite *StatusSuite) TestNone() {
suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertStatus(network.StatusSpec{})
}))
}

func (suite *StatusSuite) TestAddresses() {
nodeAddress := network.NewNodeAddress(network.NamespaceName, network.NodeAddressCurrentID)
nodeAddress.TypedSpec().Addresses = []netaddr.IP{netaddr.MustParseIP("10.0.0.1")}

suite.Require().NoError(suite.state.Create(suite.ctx, nodeAddress))

suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertStatus(network.StatusSpec{AddressReady: true})
}))
}

func (suite *StatusSuite) TestRoutes() {
route := network.NewRouteStatus(network.NamespaceName, "foo")
route.TypedSpec().Gateway = netaddr.MustParseIP("10.0.0.1")

suite.Require().NoError(suite.state.Create(suite.ctx, route))

suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertStatus(network.StatusSpec{ConnectivityReady: true})
}))
}

func (suite *StatusSuite) TestHostname() {
hostname := network.NewHostnameStatus(network.NamespaceName, network.HostnameID)
hostname.TypedSpec().Hostname = "foo"

suite.Require().NoError(suite.state.Create(suite.ctx, hostname))

suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertStatus(network.StatusSpec{HostnameReady: true})
}))
}

func (suite *StatusSuite) TestEtcFiles() {
hosts := files.NewEtcFileStatus(files.NamespaceName, "hosts")
resolv := files.NewEtcFileStatus(files.NamespaceName, "resolv.conf")

suite.Require().NoError(suite.state.Create(suite.ctx, hosts))
suite.Require().NoError(suite.state.Create(suite.ctx, resolv))

suite.Assert().NoError(retry.Constant(3*time.Second, retry.WithUnits(100*time.Millisecond)).Retry(
func() error {
return suite.assertStatus(network.StatusSpec{EtcFilesReady: true})
}))
}

func TestStatusSuite(t *testing.T) {
suite.Run(t, new(StatusSuite))
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func (ctrl *Controller) Run(ctx context.Context) error {
&network.RouteMergeController{},
&network.RouteStatusController{},
&network.RouteSpecController{},
&network.StatusController{},
&network.TimeServerConfigController{
Cmdline: procfs.ProcCmdline(),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ func NewState() (*State, error) {
&network.ResolverSpec{},
&network.RouteStatus{},
&network.RouteSpec{},
&network.Status{},
&network.TimeServerStatus{},
&network.TimeServerSpec{},
&perf.CPU{},
Expand Down
78 changes: 78 additions & 0 deletions pkg/resources/network/condition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 network

import (
"context"

"github.com/cosi-project/runtime/pkg/resource"
"github.com/cosi-project/runtime/pkg/state"
)

// ReadyCondition implements condition which waits for the network to be ready.
type ReadyCondition struct {
state state.State
checks []StatusCheck
}

// NewReadyCondition builds a coondition which waits for the network to be ready.
func NewReadyCondition(state state.State, checks ...StatusCheck) *ReadyCondition {
return &ReadyCondition{
state: state,
checks: checks,
}
}

func (condition *ReadyCondition) String() string {
return "network"
}

// Wait implements condition interface.
func (condition *ReadyCondition) Wait(ctx context.Context) error {
_, err := condition.state.WatchFor(
ctx,
resource.NewMetadata(NamespaceName, StatusType, StatusID, resource.VersionUndefined),
state.WithCondition(func(r resource.Resource) (bool, error) {
if resource.IsTombstone(r) {
return false, nil
}

status := r.(*Status).TypedSpec()

for _, check := range condition.checks {
if !check(status) {
return false, nil
}
}

return true, nil
}),
)

return err
}

// StatusCheck asserts specific part of Status to be true.
type StatusCheck func(*StatusSpec) bool

// AddressReady checks if address is ready.
func AddressReady(spec *StatusSpec) bool {
return spec.AddressReady
}

// ConnectivityReady checks if connectivity is ready.
func ConnectivityReady(spec *StatusSpec) bool {
return spec.ConnectivityReady
}

// HostnameReady checks if hostname is ready.
func HostnameReady(spec *StatusSpec) bool {
return spec.HostnameReady
}

// EtcFilesReady checks if etc files are ready.
func EtcFilesReady(spec *StatusSpec) bool {
return spec.EtcFilesReady
}
Loading

0 comments on commit 02bd657

Please sign in to comment.