framework

package
v0.0.20 Latest Latest
Warning

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

Go to latest
Published: Jun 14, 2023 License: Apache-2.0 Imports: 32 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DeleteValue

func DeleteValue(obj *unstructured.Unstructured, path ...string) error

DeleteValue (element, array or section) from the unstructured object

Requires a path consisting of nested element names e.g. "metadata", "labels", "some_label". Returns an error if nesting element is not a section. There is no error if the element being deleted does not exist, because that would be a no-op anyway. By "section" I mean map[string]interface{} i.e. map capable of nesting elements. No support for addressing individual elements in an array (slice).

func LoadInto

func LoadInto(r io.Reader, into interface{}) error

LoadInto load an object from the textual data into an existing K8s object

This function supports both strong-typed object and Unstructured object. For example to load a deployment file the following can be used:

dep := appsv1.Deployment{}
if err := framework.LoadInto(bufio.NewReader(file), &dep); err != nil { . . .

The benefits of using strong-typed objects (rather than unstructured.Unstructured) are:

  • K8s will validate the input file while loading it
  • Individual elements of the service definition will be easier to work with
  • Function framework.WaitFor() provides special handling waiting on pods for types which can own them

Both JSON and YAML formats are supported.

func LoadUnstructured

func LoadUnstructured(r io.Reader) (*unstructured.Unstructured, error)

LoadUnstructured load an object from the textual data

Using standard Reader interface (i.e. could be a file, hardcoded text, something else). Note: Unstructured object supports conversion to UnstructuredList for any object which contains "items" map directly under the object root. Both JSON and YAML formats are supported.

func ReadValue

func ReadValue(obj *unstructured.Unstructured, path ...string) (interface{}, error)

ReadValue (element, array or section) from an unstructured object

Requires a path consisting of nested element names e.g. "metadata", "labels", "some_label". Returns an error if element is not found or if a nesting element is not a section. By "section" I mean map[string]interface{} i.e. map capable of nesting elements. No support for addressing individual elements in an array (slice).

func Start

func Start(options Options, sinks *Sinks, scheme *runtime.Scheme)

Start function will prepare and start the test cluster and create the K8s client for operating on it

This is arguably the key function of the test framework, since it performs most work:

  • validation of all "glue" targets
  • shutting the previously running cluster (if there was one and "no cleanup" is not set)
  • starting up a test Kubernetes cluster
  • creating a controller-runtime/client.Client object for manipulating the test cluster
  • this client object will be made available for use in test code as framework.Kube.Client

Aside from the regular options, test code may also set:

  • Sinks, to programmatically receive and handle the output of "glue" targets
  • runtime.Scheme for CRD used by the test controller-runtime/client.Client object

func WaitFor

func WaitFor(wanted int, timeout time.Duration, from WaitForFn) error

WaitFor waits until "from" function returns a given number of instances

If the "from" function returns a strongly-typed K8s object of a type which can own pods, this number of instances refers to the number of ready pods. If the "from" function returns any of K8s List objects (including unstructured.UnstructuredList) this number refers to the number of items in the list. For Unstructured object, the returned number is a hardcoded 1. Function "from" can also return an integer number, which is useful if it needs to perform some specific check e.g. on data inside an object read from the test cluster. Specifics for individual K8s types are inside getReady function below.

If the "from" function does not return an expected number of instances within "timeout", this function will panic, hence failing test.

func WriteValue

func WriteValue(obj *unstructured.Unstructured, value interface{}, path ...string) error

WriteValue (element, array or section) from an unstructured object

Requires a path consisting of nested element names e.g. "metadata", "labels", "some_label". Returns an error if nesting element is not found or is not a section. By "section" I mean map[string]interface{} i.e. map capable of nesting elements. No support for addressing individual elements in an array (slice).

Types

type Handler

type Handler interface {
	Handle(cmd *exec.Cmd, wg *sync.WaitGroup)
}

This interface is used to handle the process after it's been started It is expected to call *FIRST* wg.Wait() and *THEN* cmd.Wait() This helps to ensure that the output scanners will read the full output before the pipes are closed inside cmd.Wait()

type Harness

type Harness struct {
	harness.Harness
	// Options provided to Start function
	Options Options
	// Sinks provided to Start function. If "nil" was given, then a default
	// empty instance of Sinks will be stored here.
	Sinks Sinks
	// Scheme provided to Start function. This will be "nil" if Start function
	// received a "nil" parameter (i.e there is no default Scheme)
	Scheme *runtime.Scheme
	// Kubernetes client initialised and authenticated for operations on the
	// test cluster, created inside Start function.
	Client client.Client
	// contains filtered or unexported fields
}

Harness represents the global state of the test framework.

The single global Harness object will be created by Start function (below) and made available through the global Kube object. Fields inside this object are meant for read only and should not be modified by the user.

var Kube *Harness

Kube is the global Harness object, created inside Start function.

func (*Harness) Close

func (h *Harness) Close() error

Close function will stop the test cluster

The call to this function is deferred inside Run, so users do not need to call it directly.

func (*Harness) NewTest

func (h *Harness) NewTest(t htesting.T) *Test

NewTest will prepare new test case

Each test case needs a small number of extra data:

  • test namespace, to be used in the K8s cluster by this test case
  • sequential test number, to aid parallel test execution

This function takes care to prepare both of these. Note, the user should also the call test.Setup function to actually create namespace object in the K8s cluster and populate the environment variables specific to this test case (which will be used to pass the above data to the "glue" targets)

func (*Harness) OpenManifest

func (h *Harness) OpenManifest(manifest string) (*os.File, error)

OpenManifest can be used to read files bundled with the acceptance tests

These files are expected to reside inside manifests subdirectory. The actual location of this directory can be set from the command line with -k8s.manifests option or set with DefaultManifests functional option. If not set it will default to "manifests". This path is relative to where the main_test.go file is.

This function is borrowed from github.com/dlespiau/kube-test-harness/blob/master/harness.go

func (*Harness) Run

func (h *Harness) Run(m *testing.M) int

Run function will apply test options and run the Go test cases, using m.Run()

Users are expected to use the return value from this function as a result code of the Go tests, e.g.:

os.Exit(framework.Kube.Run(m))

It should be called after Start function, which sets the options and starts the test cluster.

type Options

type Options struct {
	harness.Options
	// Makefile is the name of the makefile used for "glue" targets required by the test framework.
	// The default name is "Makefile"
	Makefile string
	// MakeDir is the path to the directory containing the Makefile. It should be set relative to main_test.go
	// file by the DefaultMakeDir functional option; this path will be converted inside Parse to absolute path
	// and then stored here. Note, the conversion to absolute path is idempotent and also performed inside
	// Start, which allows the user to pass a relative path to Start here (e.g. if they are not using Parse)
	MakeDir string
	// Prefix is the prefix of the make targets used for "glue" targets executed by the test framework.
	// It defaults to "test". Parse function will "sanitize" this name if it does not end with either of
	// underscore or minus sign by appending a minus sign. Such sanitized name will be stored here.
	// Note, this operation is idempotent and also performed inside Start.
	Prefix string
	// OperatorDelay is the delay for starting operator during tests, during which the test framework
	// will wait for the operator to fail or exit, so it can fail the test case immediately. If the
	// operator continues to run beyond this interval, it is considered to have started successfully and
	// the framework will stop paying attention to its exit status. The default value is 2s
	OperatorDelay time.Duration
	// EnvAlways is a flag used by the operator to decide what to do if the "env" glue target returned
	// environment variables which collide with the environment variables already set before the tests
	// have started. If this flag is set then the variables from the glue target will take priority.
	// It is recommended that tests use the DefaultEnvAlways functional option to enable this functionality.
	EnvAlways bool
}

Options for test framework

These options can be set from the command line, in which case they will be applied inside Parse function.

It is recommended that MakeDir and EnvAlways are set to sane defaults programmatically using functional options DefaultMakeDir and DefaultEnvAlways respectively.

If desired, test code may ignore the command line (by skipping Parse function) and populate Options entirely in code, before the call to Start function.

This also includes harness.Options from github.com/dlespiau/kube-test-harness/

func Parse

func Parse(opts ...ParseOptionFn) *Options

Parse function should be called at the start of test suite to parse the command line options provided by the user

Test code may set the default values of the test options using Default... functional options above, e.g.:

 func TestMain(m *testing.M) {
   options := framework.Parse(
     framework.DefaultMakeDir(".."),
		framework.DefaultEnvAlways(),
   )

In particular, DefaultMakeDir should be used to point to the Makefile directory where "glue" targets are defined.

This function does not have to be called, e.g. if the test code does not accept command line options. In this case MakeDir should be set directly inside the Options object and it will be transformed into absolute path inside the Start function.

We are making use of Functional Options pattern here, which works as follows:

  • Parse function takes a variadic slice of functions matching ParseOptionFn signature
  • each of these functions is responsible for adjusting a different field of ParseOptions structure
  • the Default... functional options documented directly above can be used to create the required functions
  • Parse will execute all these functions, hence adjusting default values of the command line parameters
  • when command line parameters are parsed inside Parse, such adjusted default values will be applied

type ParseOptionFn

type ParseOptionFn func(a *ParseOptions)

func DefaultEnvAlways

func DefaultEnvAlways() ParseOptionFn

DefaultEnvAlways should be used to give the "env" glue target priority to override inherited environment variables

Normally if the environment variables returned by the "env" glue target are already set in the inherited environment (e.g. in the shell where tests are run) then the framework will ignore values set by the target. This behaviour might not be desired, since it makes test results dependent on the environment set by the user (e.g. they might have KUBECONFIG already set). When "env always" option is set, then the test framework will ignore inherited environment variables when parsing "env" glue target output. It is recommended that test code should use DefaultEnvAlways(). This can be overridden by the user with command line option -k8s.env-always

func DefaultMakeDir

func DefaultMakeDir(makedir string) ParseOptionFn

DefaultMakeDir should be called by the test code to set the relative path of the "glue" makefile

For example, assuming that the "glue" targets are defined inside the Makefile residing in the project root directory and acceptance tests are in the "acceptance" directory, as demonstrated below:

| Makefile
+ acceptance
  | main_test.go

then the TestMain function defined inside main_test.go should pass DefaultMakeDir("..") to Parse function. This will ensure that framework will be able to find the Makefile in root project directory to execute the "glue" targets. The user can override this value using command line option -k8s.makedir

func DefaultMakefile

func DefaultMakefile(makefile string) ParseOptionFn

DefaultMakefile can be used to override the default name of the "glue" makefile

The default name is "Makefile", however if the "glue" targets are defined in a different makefile, the test code can set a different default using this function. The user can override this option using command line option -k8s.makefile

func DefaultManifests

func DefaultManifests(manifests string) ParseOptionFn

DefaultManifests can be called to override the default location of manifest files relative to main_test.go

If not set inside the test code, hardcoded subdirectory "manifests" will be used. The user can override this value using command line option -k8s.manifests

func DefaultNoCleanup

func DefaultNoCleanup() ParseOptionFn

DefaultNoCleanup can be called to disable cleanup after tests

This function has the same effect as command line option -k8s.no-cleanup=true , however it can be overridden by the user setting -k8s.no-cleanup=false . If "no cleanup" option is set, then the test framework will not destroy test objects in the test K8s cluster and will not shutdown the cluster. This may lead to excessive resource utilization by the test cluster and in turn to transient test failures, so it is recommended that this function is not used by the test code.

func DefaultOperatorDelay

func DefaultOperatorDelay(opdelay time.Duration) ParseOptionFn

DefaultOperatorDelay is how long the framework will wait for the operator being tested to report an error

If not set inside the test code, hardcoded 2 seconds will be used. Larger value means that operator can do more work before failing, if we want that failure to be detected by the tests immediately inside the StartOperator function; it also means that starting the operator will take longer. This can be overridden by the user with -k8s.op-delay command line option.

func DefaultPrefix

func DefaultPrefix(prefix string) ParseOptionFn

DefaultPrefix can be called to override the default prefix of "glue" targets inside Makefile

If not set inside the test code, hardcoded prefix "test" will be used. The user can override this value using command line option -k8s.prefix. If the the prefix does not end with either of minus or underscore sign then the framework will automatically add minus character at the end. Empty prefix is also valid.

The purpose of prefix is to separate targets used as "glue" by tests and other targets which might be defined in the same Makefile. For example, in order to prepare environment variables before test cluster is started the test framework will invoke "env" glue target, which together with the default prefix makes "test-env" target in the Makefile.

func OverrideCmdLine

func OverrideCmdLine(cmdline *flag.FlagSet) ParseOptionFn

OverrideCmdLine can be used by test code to override the default FlagSet used for tests

Normally tests should not use this function. It is only meant for testing of the Parse function.

func OverrideOsArgs

func OverrideOsArgs(osargs []string) ParseOptionFn

OverrideOsArgs can be used by the test code to override command line parameters passed to tests

Normally tests should not use this function, unless they really want to ignore command line parameters, but still want to call Parse function (rather than prepare Options object explicitly in code). This function is mainly used for testing of the Parse function.

type ParseOptions

type ParseOptions struct {
	MakeDir       string
	Makefile      string
	Manifests     string
	Prefix        string
	NoCleanup     bool
	OperatorDelay time.Duration
	EnvAlways     bool
	OsArgs        []string
	CmdLine       *flag.FlagSet
}

ParseOptions is used by Parse function to enable the test code to set own defaults

We are applying Functional Options pattern, described by Rob Pike at https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html (first half only) and further documented by Dave Cheney at https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis A short version of this pattern is described with Parse function below.

The purpose of functional options in Parse is to customise the default parameters before command line parsing. This helps the users to avoid long "go test" invocations with many "-k8s..." parameters, by enabling the test code to provide the most useful defaults instead.

Each of the options below is set by some of the Default... functions below, and then applied inside Parse together with command line options to create an Options object which can be passed to Start function.

type Sinks

type Sinks struct {
	Stdout   []io.Writer
	Stderr   []io.Writer
	Operator []io.Writer
}

Sinks can be used to capture the "console" output from the spawned sub-processes (e.g. glue targets or custom targets) for the purpose of testing. Capturing this output may be useful in tests.

type Test

type Test struct {
	harness.Test
	// contains filtered or unexported fields
}

func (*Test) Close

func (t *Test) Close()

Close should be called at the end of each test case

Ideally this function should be "deferred" right after test.Setup, as demonstrated above.

func (*Test) DeleteDeployment

func (t *Test) DeleteDeployment(d *appsv1.Deployment, timeout time.Duration)

DeleteDeployment is a shortcut function for deleting a deployment and waiting until it is deleted

Note: this function is considered "alpha" and may get deleted or replaced with a different "helper" function.

func (*Test) RunTarget

func (t *Test) RunTarget(name string, env ...map[string]string) error

RunTarget will invoke arbitrary target from "glue" makefile

Note, the actual target name will be prefixed like all other "glue" targets, so assuming default "test" prefix this call test.RunTarget("stuff") will run "test-stuff" target. Extra environment variables can be passed to the invoked target, using the map parameters. These parameters are applied from left to right and never override variables set earlier. In particular, environment variables set inside test.Setup() cannot be overridden. RunTarget function will return an error if the target cannot be found or if it failed execution for any reason.

func (*Test) Setup

func (t *Test) Setup() *Test

Setup performs necessary initialisation of test case

This function should be called at the start of every test case and followed by "defer test.Close()", e.g:

test := framework.Kube.NewTest(t).Setup()
defer test.Close()

func (*Test) StartOperator

func (t *Test) StartOperator() error

StartOperator starts the operator process for test case

This function will invoke "operator start" glue target, which will be next monitored in a dedicated Go routine. Test code can receive console output from the operator by setting Operator element of Sinks type when the test framework is initialized with framework.Start function. If the operator process terminates before DefaultOperatorDelay (or -k8s.op-delay) has elapsed, StartOperator function will return an error.

func (*Test) StopOperator

func (t *Test) StopOperator()

StopOperator stops the operator process

The operator does not have to be running - trying to stop an operator when it is not running is an no-op. It is OK to start the operator again after it's been stopped, within the scope of same test. There is no need to stop operator explicitly at the end of test using this function, since Close function will do the same.

type WaitForFn

type WaitForFn func() (interface{}, error)

Jump to

Keyboard shortcuts

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