monax

package module
v0.0.0-...-1899be6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 3, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

README

CI License

Monax

Monax is a System Under Test (SUT) manager. It is intended to work with Ondatra for writing integration tests that verify the interaction between network management software systems and the network devices that they control.

Background

Ondatra lets you write functional tests for network devices (referred to as devices under test, or DUTs). Monax lets you add a SUT to those tests so you can do integration testing of services that interact with the DUTs. An Ondatra functional test will make sure the device works in the way a network engineer expects. A Monax integration test will make sure the device and services interact in the way that network architects expect.

In Ondatra, tests are not hardcoded to specific devices. Instead, you give an abstract definition for the kinds of devices you want to test. Ondatra then maps that abstract testbed definition to actual devices. Because you write your test against the abstract definition and not specific devices, the test is more flexible.

Monax works similarly. Instead of hardcoding specific services in your test, you give an abstract definition for your SUT and test against that abstraction. Monax will map that abstract SUT definition to actual services. This separation means that your integration tests can now be flexible too.

Example

Let's say we are developing a health service to monitor router health across a corporate network spanning a couple of Points of Presence (POPs). The service uses gNMI to monitor the health of devices and links. We want to have good test coverage of the interaction between the devices and the service so that we have confidence that our network is healthy when the service says the network is healthy.

Health Service Example

To start, let's write functional tests to make sure that each device provides the right health information that we care about. We set up test devices to a known state similar to the real network and make sure that they report metrics as expected. Then we bring the device down and make sure that its metrics are unhealthy.

Health Service Functional Test

We can write this test with Ondatra by itself. It verifies the device behaves as we expect it to. We also need to make sure that the interaction with the service is correct. To do this, we can write an integration test with Monax and Ondatra. We set up the test devices as before, but we additionally set up a SUT containing an instance of our health service. We configure the health service to monitor the test devices. We then make sure the health service is reporting that our test network is healthy. Then we can bring the device down and make sure that the health service responds appropriately.

Health Service Integration Test

Getting Started

The math test example uses the Kubernetes runtime to start simple gRPC servers in an existing Kubernetes cluster. The servers provide simple math operations. The test makes calls to those servers to verify their functionality.

For more information on how to run the math test example, please see the README.

Why is it called Monax?

The groundhog (Marmota monax) is a type of large ground squirrel found throughout much of North America. Also known as woodchucks, whistle pigs, land beavers, and several other names, groundhogs are famous for their ornery behavior and for predicting the arrival of spring on Groundhog Day.

In the movie Groundhog Day, the main character becomes trapped in a time loop, reliving the same day repeatedly. No matter how destructive his actions are, his world resets every morning, exactly like it was the day before.

Integration testing should work the same way. No matter what your test does or how badly it misbehaves, your system under test should be ready to go for your next run, exactly like it was before.

As Phil might say, Don't test angry.

Documentation

Overview

Package monax represents the public Monax API.

Index

Constants

View Source
const ComponentIDRegex = `^[a-zA-Z][a-zA-Z0-9-_]*$`

ComponentIDRegex is the regex for a valid component ID. The ID must start with a letter, and may only contain letters, numbers, dashes, and underscores. Whitespace is not allowed due to how the CLIs and other tools parse args with spaces.

View Source
const EmptyTarget = Target("")

EmptyTarget represents an empty target.

Variables

View Source
var (
	// ErrDuplicateInterfaceDefinitions indicates the library is invalid because
	// it contains a component with duplicate interface definitions.
	ErrDuplicateInterfaceDefinitions = errors.New("duplicate interface definitions")

	// ErrProvidedInterface indicates that an error was received from the
	// component handler when trying to retrieve a provided interface.
	ErrProvidedInterface = errors.New("retrieving provided interface")

	// ErrUnknownRequiredInterfaceName indicates that the required interface name
	// could not be found in the component's required interfaces.
	ErrUnknownRequiredInterfaceName = errors.New("unknown required interface name")

	// ErrInvalidComponentID indicates that the component ID in the library is
	// invalid.
	ErrInvalidComponentID = errors.New("invalid component ID")
)
View Source
var (
	// ErrDecodeTextproto indicates a textproto could not be decoded to a specific
	// protobuf message.
	ErrDecodeTextproto = errors.New("decode textproto")

	// ErrNoAbstractSUT indicates an abstract SUT was not set in the Config.
	ErrNoAbstractSUT = errors.New("no abstract SUT in config")

	// ErrNoLibrary indicates a library was not set in the Config.
	ErrNoLibrary = errors.New("no library in config")

	// ErrReadTextproto indicates a textproto file could not be read.
	ErrReadTextproto = errors.New("read textproto")
)
View Source
var (

	// ErrDialGRPCService indicates that the gRPC service could not be dialed.
	ErrDialGRPCService = errors.New("dial gRPC service")

	// ErrVerifyGRPCService indicates that the gRPC service could not be verified.
	ErrVerifyGRPCService = errors.New("verify gRPC service")
)
View Source
var (
	// ErrCyclicDependencies indicates the library is invalid because it contains
	// cyclic dependencies.
	ErrCyclicDependencies = errors.New("cyclic dependencies")

	// ErrDuplicateComponentDefinitions indicates the library is invalid because
	// it contains duplicate definitions of a component.
	ErrDuplicateComponentDefinitions = errors.New("duplicate component definitions")

	// ErrDuplicateInterfaceProviders indicates the library is invalid because it
	// contains duplicate providers of an interface.
	ErrDuplicateInterfaceProviders = errors.New("duplicate interface providers")

	// ErrMissingHandler indicates the library is invalid because it contains
	// components with no handler in the runtime.
	ErrMissingHandler = errors.New("missing handler")

	// ErrMissingRequiredInterface indicates the abstract SUT or library are
	// invalid because an interface required by the abstract SUT or a component in
	// the library is not provided by the library.
	ErrMissingRequiredInterface = errors.New("missing required interface")

	// ErrNoConfig indicates a Config was not set.
	ErrNoConfig = errors.New("no config")

	// ErrNoRequiredInterfaces indicates the abstract SUT is invalid because it
	// specifies no required interfaces.
	ErrNoRequiredInterfaces = errors.New("no required interfaces")

	// ErrNoNewRuntimeFn indicates a new runtime function was not set.
	ErrNoNewRuntimeFn = errors.New("no new runtime function")

	// ErrNoRuntime indicates a runtime was not returned by the new runtime
	// function.
	ErrNoRuntime = errors.New("no runtime")
)
View Source
var (
	// ErrAlreadyStarted indicates the SUT cannot perform an operation because it
	// is already started.
	ErrAlreadyStarted = errors.New("already started")

	// ErrNotStarted indicates the SUT cannot perform an operation because it is
	// not started.
	ErrNotStarted = errors.New("not started")
)
View Source
var (
	// ErrRequestedInterfaceNotProvided indicates that the requested interface is
	// not provided by the SUT because it was not specified as a required
	// interface of the abstract SUT.
	ErrRequestedInterfaceNotProvided = errors.New("requested interface not provided by the SUT")
)
View Source
var (
	// ErrTargetNotImplemented indicates that a handler has not implemented the
	// requested target retriever type.
	ErrTargetNotImplemented = errors.New("target not implemented")
)

Functions

func DHCPSelector

func DHCPSelector(serviceName string) func(*monaxpb.Interface) bool

DHCPSelector returns a function that matches a DHCP interface with the given service name.

func GRPCSelector

func GRPCSelector(serviceName string) func(*monaxpb.Interface) bool

GRPCSelector returns a function that matches a gRPC interface with the given service name.

func HTTPSSelector

func HTTPSSelector(serviceName string) func(*monaxpb.Interface) bool

HTTPSSelector returns a function that matches an HTTPS interface with the given service name.

func HTTPSelector

func HTTPSelector(serviceName string) func(*monaxpb.Interface) bool

HTTPSelector returns a function that matches an HTTP interface with the given service name.

func ProcessVars

func ProcessVars(global map[string]string, local map[string]string, component *Component) (map[string]string, error)

ProcessVars replaces templated strings in the local map with the appropriate values from the global or local map or the appropriate dependency target.

Types

type Component

type Component struct {
	// contains filtered or unexported fields
}

Component is an element in the SUT that satisfies direct or transitive dependencies of the abstract SUT.

func (*Component) ID

func (c *Component) ID() string

ID returns the component id.

func (*Component) Parameters

func (c *Component) Parameters() *anypb.Any

Parameters returns the component parameters.

func (*Component) RequiredTarget

func (c *Component) RequiredTarget(name string) (Target, error)

RequiredTarget returns the target for the given required interface name. Required interface names are specified by the user in the component proto.

func (*Component) ResolvePath

func (c *Component) ResolvePath(path string) string

ResolvePath resolves a textproto path using the relative path information injected into the component when the library is built.

func (*Component) String

func (c *Component) String() string

type Config

type Config struct {
	// An abstract SUT is a description of the high-level requirements of a test.
	//
	// One of AbstractSUT or AbstractSUTPath must be set in the config.  If both
	// are set, AbstractSUT will be used and AbstractSUTPath will be ignored.
	// AbstractSUTPath is a path to a monax.AbstractSut textproto and may be set
	// from the command line.
	AbstractSUT     *monaxpb.AbstractSut
	AbstractSUTPath string

	// A library contains descriptions of the components available to satisfy the
	// requirements of the abstract SUT.
	//
	// One of Library or LibraryPath must be set in the config.  If both are set,
	// Library will be used and LibraryPath will be ignored. LibraryPath is a path
	// to a monax.Library textproto and may be set from the command line.
	Library     *monaxpb.Library
	LibraryPath string

	// Runtime parameters are additional configuration options for the runtime.
	//
	// RuntimeParameters or RuntimeParametersPath may be set in the config.  If
	// both are set, RuntimeParameters will be used and RuntimeParametersPath will
	// be ignored.  RuntimeParametersPath is a path to a monax.RuntimeParameters
	// and may be set from the command line.
	RuntimeParameters     *monaxpb.RuntimeParameters
	RuntimeParametersPath string
	// contains filtered or unexported fields
}

A Config contains the parameters used to create a new SUT.

The parameters can be set programmatically. Some parameters may be set with command line flags, as in the following example.

package main

var config monax.Config

func init() {
	config.RegisterFlags(nil) // install the flags
}

func main() {
	flag.Parse()
	ctx := context.Background()
	runtime := ...
	sut, err := monax.New(ctx, &config, runtime)
	// check err and use sut ...
}

func (*Config) RegisterFlags

func (c *Config) RegisterFlags(fs *flag.FlagSet)

RegisterFlags registers command line flags that modify a Config. flag.CommandLine is used when the flag set is nil.

type Handler

type Handler interface {
	Initialize(ctx context.Context, component *Component) error
	Start(ctx context.Context, component *Component) error
	Stop(ctx context.Context, component *Component) error
	Status(ctx context.Context, component *Component) error
	Targets(component *Component) Targets
}

Handler is an interface for interacting with a component within the SUT.

type NewRuntimeFn

type NewRuntimeFn func() (Runtime, error)

NewRuntimeFn defines the function signature for creating new instances of a Runtime. It's used to allow different runtime implementations to be registered and created dynamically.

type Runtime

type Runtime interface {
	Initialize(parameters *monaxpb.RuntimeParameters) error
	Handler(kind string) Handler
}

Runtime is an interface for runtime implementations.

type SUT

type SUT struct {
	// contains filtered or unexported fields
}

SUT is a system under test.

func New

func New(ctx context.Context, config *Config, newRuntimeFn NewRuntimeFn) (*SUT, error)

New makes a new SUT from a Config and Runtime.

func (*SUT) Interfaces

func (s *SUT) Interfaces() *SUTInterfaces

Interfaces returns a handle to the interfaces API.

func (*SUT) Start

func (s *SUT) Start(ctx context.Context) error

Start starts the components in the SUT. If component A depends on an interface provided by component B, A will be started only after B has started.

Returns ErrAlreadyStarted if the SUT is already started. Otherwise, returns a SUTError if any component fails to start.

Caller should stop the SUT when done with it by calling [Stop].

if err := sut.Start(ctx); err != nil {
	// handle error
}
defer func() {
	if err := sut.Stop(ctx); err != nil {
		// handle error
	}
}

If the error will be handled by exiting the program, including by log.Exit or log.Fatal, then the caller should stop the SUT prior to exiting.

if err := sut.Start(ctx); err != nil {
	err = errors.Join(err, sut.Stop(ctx))
	log.ExitContext(ctx, err)
}
defer func() {
	if err := sut.Stop(ctx); err != nil {
		// handle error
	}
}

func (*SUT) Started

func (s *SUT) Started() bool

Started returns true if the SUT is started.

func (*SUT) Status

func (s *SUT) Status(ctx context.Context) error

Status gets the status of the components in the SUT.

Returns ErrNotStarted if the SUT is not started. Otherwise, returns a SUTError if any component has errors.

func (*SUT) Stop

func (s *SUT) Stop(ctx context.Context) error

Stop stops the components in the SUT. If component A depends on an interface provided by component B, B will be stopped only after A has stopped.

If Stop fails, the SUT may need to be stopped manually. See [Start] for examples for calling Stop.

Returns ErrNotStarted if the SUT is not started. Otherwise, returns a SUTError if any component fails to stop.

func (*SUT) String

func (s *SUT) String() string

func (*SUT) Targets

func (s *SUT) Targets() *SUTTargets

Targets returns a handle to the targets API.

type SUTError

type SUTError struct {
	ErrorByComponent map[*Component]error
}

SUTError is an aggregation of component errors.

func (*SUTError) Error

func (err *SUTError) Error() string

func (*SUTError) PrettyPrint

func (err *SUTError) PrettyPrint() string

PrettyPrint returns a human readable string representation of the SUTError.

type SUTInterfaces

type SUTInterfaces struct {
	// contains filtered or unexported fields
}

SUTInterfaces implements methods for retrieving different kinds of interfaces from the SUT.

func (*SUTInterfaces) DHCP

func (i *SUTInterfaces) DHCP(ctx context.Context, serviceName string) (string, error)

DHCP returns the URL of the DHCP service with the given serviceName from the SUT.

func (*SUTInterfaces) GRPC

func (i *SUTInterfaces) GRPC(ctx context.Context, serviceName string) (*grpc.ClientConn, error)

GRPC returns a connection to the gRPC service with the given serviceName from the SUT.

func (*SUTInterfaces) HTTP

func (i *SUTInterfaces) HTTP(ctx context.Context, serviceName string) (string, error)

HTTP returns the URL of the HTTP service with the given serviceName from the SUT.

func (*SUTInterfaces) HTTPS

func (i *SUTInterfaces) HTTPS(ctx context.Context, serviceName string) (string, error)

HTTPS returns the URL of the HTTPS service with the given serviceName from the SUT.

type SUTTargets

type SUTTargets struct {
	// contains filtered or unexported fields
}

SUTTargets implements methods for retrieving different kinds of targets from the SUT.

func (*SUTTargets) DHCP

func (t *SUTTargets) DHCP(ctx context.Context, serviceName string) (string, error)

DHCP returns the target of the DHCP service with the given serviceName from the SUT.

func (*SUTTargets) GRPC

func (t *SUTTargets) GRPC(ctx context.Context, serviceName string) (string, error)

GRPC returns the target of the gRPC service with the given serviceName from the SUT.

func (*SUTTargets) HTTP

func (t *SUTTargets) HTTP(ctx context.Context, serviceName string) (string, error)

HTTP returns the target of the HTTP service with the given serviceName from the SUT.

func (*SUTTargets) HTTPS

func (t *SUTTargets) HTTPS(ctx context.Context, serviceName string) (string, error)

HTTPS returns the target of the HTTPS service with the given serviceName from the SUT.

type Target

type Target string

Target represents the target for an interface.

type Targets

type Targets interface {
	DHCP(ctx context.Context, serviceName string) (Target, error)
	GRPC(ctx context.Context, serviceName string) (Target, error)
	HTTP(ctx context.Context, serviceName string) (Target, error)
	HTTPS(ctx context.Context, serviceName string) (Target, error)
}

Targets is an interface for retrieving targets of provided interfaces in the SUT.

type UnimplementedTargets

type UnimplementedTargets struct{}

UnimplementedTargets is a struct that implements Targets and returns ErrTargetNotImplemented for all target types. By embedding this struct, implementations of Targets can ignore target types that they do not support.

func (UnimplementedTargets) DHCP

func (UnimplementedTargets) DHCP(ctx context.Context, serviceName string) (Target, error)

DHCP returns ErrTargetNotImplemented.

func (UnimplementedTargets) GRPC

func (UnimplementedTargets) GRPC(ctx context.Context, serviceName string) (Target, error)

GRPC returns ErrTargetNotImplemented.

func (UnimplementedTargets) HTTP

func (UnimplementedTargets) HTTP(ctx context.Context, serviceName string) (Target, error)

HTTP returns ErrTargetNotImplemented.

func (UnimplementedTargets) HTTPS

func (UnimplementedTargets) HTTPS(ctx context.Context, serviceName string) (Target, error)

HTTPS returns ErrTargetNotImplemented.

Directories

Path Synopsis
Package monaxondatratest provides a utility function to start and stop the SUT for Monax tests within the Ondatra test lifecycle.
Package monaxondatratest provides a utility function to start and stop the SUT for Monax tests within the Ondatra test lifecycle.
Package monaxtest provides a utility function to start the SUT for Monax tests.
Package monaxtest provides a utility function to start the SUT for Monax tests.
Package proto contains protos for monax.
Package proto contains protos for monax.
runtime
kubernetesruntime
Package kubernetesruntime is the Monax runtime for Kubernetes components.
Package kubernetesruntime is the Monax runtime for Kubernetes components.
kubernetesruntime/proto
Package proto contains protos for the kubernetes runtime.
Package proto contains protos for the kubernetes runtime.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL