Click or drag to resize

KubeService Class

Base class for Kubernetes services that wish to use the neonKUBE unit testing conventions.
Inheritance Hierarchy
SystemObject
  Neon.Kube.ServiceKubeService

Namespace:  Neon.Kube.Service
Assembly:  Neon.Kube.Service (in Neon.Kube.Service.dll) Version: 2.1.0
Syntax
public abstract class KubeService : IDisposable

The KubeService type exposes the following members.

Constructors
  NameDescription
Public methodKubeService
Constructor.
Top
Properties
  NameDescription
Public propertyArguments
Returns the list of command line arguments passed to the service. This defaults to an empty list.
Public propertyBaseUri

For services with exactly one network endpoint, this returns the base URI to be used to access the service.

Note Note
This will throw a InvalidOperationException if the service defines no endpoints or has multiple endpoints.
Public propertyDescription
Returns the service description for this service.
Public propertyEndpoints
Returns the dictionary mapping case sensitive service endpoint names to endpoint information.
Public propertyExitCode
Returns the exit code returned by the service.
Public propertyExitException
Returns any abnormal exception thrown by the derived OnRunAsync method.
Public propertyGitVersion
Returns GIT branch and commit the service was built from as well as an optional indication the the build branch had uncomitted changes (e.g. was dirty).
Public propertyInDevelopment
Returns true when the service is running in development or test mode, when the DEV_WORKSTATION environment variable is defined.
Public propertyInProduction
Returns true when the service is running in production, when the DEV_WORKSTATION environment variable is not defined.
Public propertyLog
Returns the service's default logger.
Public propertyLogManager
Returns the service's log manager.
Public propertyName
Returns the service name.
Public propertyServiceMap
Returns the service map.
Public propertyStatus
Returns the service current running status.
Public propertyTerminator
Returns the service's ProcessTerminator. This can be used to handle termination signals.
Top
Methods
  NameDescription
Public methodDispose
Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
Protected methodDispose(Boolean)
Releases all associated resources.
Public methodEquals
Determines whether the specified object is equal to the current object.
(Inherited from Object.)
Public methodExit
Used by services to stop themselves, specifying an optional process exit code.
Protected methodFinalize
Finalizer.
(Overrides ObjectFinalize.)
Public methodGetConfigFilePath
Returns the physical path for the confguration file whose logical path is specified.
Public methodGetEnvironmentVariable
Returns the value of an environment variable.
Public methodGetHashCode
Serves as the default hash function.
(Inherited from Object.)
Public methodGetType
Gets the Type of the current instance.
(Inherited from Object.)
Public methodLoadEnvironmentVariables

Loads environment variables formatted as NAME=VALUE from a text file as service environment variables. The file will be decrypted using NeonVault if necessary.

Note Note
Blank lines and lines beginning with '#' will be ignored.
Protected methodMemberwiseClone
Creates a shallow copy of the current Object.
(Inherited from Object.)
Protected methodOnRunAsync
Called to actually implement the service.
Public methodRunAsync
Starts the service if it's not already running. This will call OnRunAsync, which actually implements the service.
Public methodSetArguments
Initializes Arguments with the command line arguments passed.
Public methodSetConfigFile(String, Byte)
Maps a logical configuration file path to a temporary file holding the byte contents passed. This is typically used initializing confguration files for unit testing.
Public methodSetConfigFile(String, String, Boolean)
Maps a logical configuration file path to a temporary file holding the string contents passed encoded as UTF-8. This is typically used for initializing confguration files for unit testing.
Public methodSetConfigFilePath
Maps a logical configuration file path to an actual file on the local machine. This is used for unit testing to map a file on the local workstation to the path where the service expects the find to be.
Public methodSetEnvironmentVariable
Sets or deletes a service environment variable.
Public methodSetRunningAsync
Called by OnRunAsync implementation after they've completed any initialization and are ready for traffic. This sets Status to Running.
Public methodSetStatusAsync
Updates the service status. This is typically called internally by this class but service code may set this to Unhealthy when there's a problem and back to Running when the service is healthy again.
Public methodStop

Stops the service if it's not already stopped. This is intended to be called by external things like unit test fixtures and is not intended to be called by the service itself.

Public methodToString
Returns a string that represents the current object.
(Inherited from Object.)
Top
Fields
  NameDescription
Public fieldStatic memberGlobalLogging
This controls whether any KubeService instances will use the global Default log manager for logging or maintain its own log manager. This defaults to true which will be appropriate for most production situations. It may be useful to disable this for some unit tests.
Top
Remarks

Basing your service implementations on the Neon.Kube.Service class will make them easier to test via integration with the ServiceFixture from the Neon.Xunit library by providing some useful abstractions over service configuration, startup and shutdown including a ProcessTerminator to handle termination signals from Kubernetes.

This class is pretty easy to use. Simply derive your service class from KubeService and implement the OnRunAsync method. OnRunAsync will be called when your service is started. This is where you'll implement your service. You should perform any initialization and then call SetRunningAsync to indicate that the service is ready for business.

Note Note
Note that calling SetRunningAsync after your service has initialized is very important because the KubeServiceFixture requires won't allow tests to proceed until the service indicates that it's ready. This is necessary to avoid unit test race conditions.

Note that your OnRunAsync method should generally not return until the Terminator signals it to stop. Alternatively, you can throw a ProgramExitException with an optional process exit code to proactively exit your service.

Note Note
All services should properly handle Terminator stop signals so unit tests will terminate promptly. Your terminate handler method must return within a set period of time (30 seconds by default) to avoid having your tests being forced to stop. This is probably the trickiest implementation task. For truly asynchronous service implementations, you should consider passing the CancellationToken to all async methods you call and then handle any TaskCanceledException exceptions thrown by returning from OnRunAsync.
Note Note
This class uses the DEV_WORKSTATION environment variable to determine whether the service is running in test mode or not. This variable will typically be defined on developer workstations as well as CI/CD machines. This variable must never be defined for production environments. You can use the InProduction or InDevelopment properties to check this.

CONFIGURATION

Services are generally configured using environment variables and/or configuration files. In production, environment variables will actually come from the environment after having been initialized by the container image or passed by Kubernetes when starting the service container. Environment variables are retrieved by name (case sensitive).

Configuration files work the same way. They are either present in the service container image or mounted to the container as a secret or config file by Kubernetes. Configuration files are specified by their path (case sensitive) within the running container.

This class provides some abstractions for managing environment variables and configuration files so that services running in production and services running in a local unit test can configure themselves using the same code for both environments.

Services should use the GetEnvironmentVariable(String, String) method to retrieve important environment variables rather than using GetEnvironmentVariable(String). In production, this simply returns the variable directly from the current process. For tests, the environment variable will be returned from a local dictionary that was expicitly initialized by calls to SetEnvironmentVariable(String, String). This local dictionary allows the testing of multiple services at the same time with each being presented their own environment variables.

You may also use the LoadEnvironmentVariables(String, FuncString, String) method to load environment variables from a text file (potentially encrypted via NeonVault). This will typically be done only for unit tests.

Configuration files work similarily. You'll use GetConfigFilePath(String) to map a logical file path to a physical path. The logical file path is typically specified as the path where the configuration file will be located in production. This can be any valid path with in a running production container and since we're currently Linux centric, will typically be a Linux file path like /etc/MYSERVICE.yaml or /etc/MYSERVICE/config.yaml.

For production, GetConfigFilePath(String) will simply return the file path passed so that the configuration file located there will referenced. For testing, GetConfigFilePath(String) will return the path specified by an earlier call to SetConfigFilePath(String, String, FuncString, String) or to a temporary file initialized by previous calls to SetConfigFile(String, String, Boolean) or SetConfigFile(String, Byte). This indirection provides a consistent way to run services in production as well as in tests, including tests running multiple services simultaneously.

SERVICE TERMINATION

All services, especially those that create unmanaged resources like ASP.NET services, sockets, NATS clients, HTTP clients, thread etc. should override and implement Dispose(Boolean) to ensure that any of these resources are proactively disposed. Your method should call the base class version of the method first before disposing these resources.

C#
protected override Dispose(bool disposing)
{
    base.Dispose(disposing);

    if (appHost != null)
    {
        appHost.Dispose();
        appHost = null;
    }
}

The disposing parameter is passed as true when the base Dispose method was called or false if the garbage collector is finalizing the instance before discarding it. The difference is subtle and most services can safely ignore this parameter (other than passing it through to the base Dispose(Boolean) method).

In the example above, the service implements an ASP.NET web service where appHost was initialized as the IWebHost actually implementing the web service. The code ensures that the appHost isn't already disposed before disposing it. This will stop the web service and release the underlying listening socket. You'll want to do something like this for any other unmanaged resources your service might hold.

Note Note

It's very important that you take care to dispose things like running web services and listening sockets within your Dispose(Boolean) method. You also need to ensure that any threads you've created are terminated. This means that you'll need a way to signal threads to exit and then wait for them to actually exit.

This is important when testing your services with a unit testing framework like Xunit because frameworks like this run all tests within the same Test Runner process and leaving something like a listening socket open on a port (say port 80) may prevent a subsequent test from running successfully due to it not being able to open its listening socket on port 80.

LOGGING

Each KubeService instance maintains its own LogManager instance with the a default logger created at Log. The log manager is initialized using the LOG_LEVEL environment variable value which defaults to info when not present. LogLevel for the possible values.

Note that the Default log manager will also be initialized with the log level when the service is running in a production environment so that logging in production works completely as expected.

For development environments, the Default instance's log level will not be modified. This means that loggers created from Default may not use the same log level as the service itself. This means that library classes that create their own loggers won't honor the service log level. This is an unfortunate consequence of running emulated services in the same process.

There are two ways to mitigate this. First, any source code defined within the service project should be designed to create loggers from the service's LogManager rather than using the global one. Second, you can configure your unit test to set the desired log level like:

C#
LogManager.Default.SetLogLevel(LogLevel.Debug));
Note Note
Setting the global default log level like this will impact loggers created for all emulated services, but this shouldn't be a problem for more situations.

HEALTH PROBES

Hosting environments such as Kubernetes will often require service instances to be able to report their health via health probes. These probes are typically implemented as a script that is called periodically by the hosting environment with the script return code indicating the service instance health.

The KubeService class supports this by optionally writing a text file with various strings indicating the health status. This file will consist of a single line of text without line ending characters. You'll need to specify the fully qualified path to this file as an optional parameter to the KubeService constructor.

See Also