Creating Project Templates for dotnet – Part 1 – Getting Started

This is part 1 of a 4 part series of posts


Custom project templates can be a great time saver for you and your team and the can promote a level of consistency across all of your projects. In my experience within any given organization a great deal of effort is wasted every time a new project is started creating the new project, writing plumbing code to handle routine, cross cutting concerns like logging, health checks, authentication/authorization as well as just generally configuring the project layout to conform with your teams practices, importing common libraries & corporate style sheets, setting up continuous integration pipelines etc. By doing all this once up front and baking it into a project template you can save that time each time a new project is started and the team can get down to providing actual business value almost immediately.

For this post I’m going to walk through creating a new template for an ASP.NET 5.0 MVC Web Application. We’ll start off with the default template and modify from there over the next few posts.

Getting Started / Setting Up the Project Structure

To get started we’ll create a new repository in source control (GitHub in my case) and clone it.

Initial Repository Listing

Next we let’s add a src folder and in that folder we’ll add a content folder that will contain the contents of our template. Ultimately this content folder will become the content folder within a NuGet package for our template.

md src
cd src
md content
cd content

When my template is run, I want it to create a src folder with my web application in it and a tests folder with my unit tests. To accomplish that we’ll create a new src folder and then use the existing, out of the box MVC template and then create a tests folder and use the existing, out of the box Xunit template.

md src
cd src
dotnet new mvc -n DotNetNinja.Templates.Mvc
cd ..
md tests
cd tests
dotnet new xunit -n DotNetNinja.Templates.Mvc.Tests
cd ..

Now let’s create a solution file, add the projects to it and create the project reference from the unit tests to the web project.

dotnet new sln -n DotNetNinja.Templates.Mvc
dotnet sln add .\src\DotNetNinja.Templates.Mvc
dotnet sln add .\tests\DotNetNinja.Templates.Mvc.Tests
dotnet add .\tests\DotNetNinja.Templates.Mvc.Tests reference .\src\DotNetNinja.Templates.Mvc

At this point we should have a fully working solution created. One of the great things about the new template system is that your templates are actually functioning projects that can be built and run. That also means that all of the tools that you use for day to day development like Intellisense, syntax highlighting, and tools like Resharper can all be used during your template development process making it much easier and more intuitive to create quality templates.

Now that we have scaffolded out the entire solution we can go ahead and open it up in Visual Studio and see what we have to start with.

Making Our Solution a Template

Having laid out the basic structure we are looking for we have a good foundation. Let’s go ahead and convert it to a template. In our content folder we’ll need to add a .template.config folder and place a template.json file in that folder.

md .template.config
cd .template.config
New-Item template.json

Next let’s open the template.json file up, add in our basic template definition and take a look at what we have.

{    
    "$schema": "http://json.schemastore.org/template",
    "author": "Larry House",
    "classifications": [ "Web" ],
    "name": "DotNetNinja MVC Template",
    "identity": "DotNetNinja.Templates.Mvc",        
    "shortName": "ninjamvc",                 
    "tags": {
        "language": "C#",    
        "type": "project"                 
    },
    "sourceName": "DotNetNinja.Templates.Mvc",
    "preferNameDirectory" : true
}
  • $schema: Sets the json schema and gives us intellisense for our json file.
  • author: Sets the author of the template.
  • classifications: An array of project types. This can be used for filtering in the Visual Studio new project dialog.
  • name: Is the friendly/display name for the template.
  • identity: Is a unique identifier for the template, I typically use the NuGet package id here.
  • shortName: is the short name for the template that is used to invoke it with dotnet new (ex. dotnet new ninjamvc)
  • tags: Tags for the template. These show up in the dotnet new list and can be used to filter in Visual Studio as well.
  • sourceName: This one is important as it specifies the name/value in the source to be automatically replaced with the name of the generated project. Typically that includes the project name, project file name, default namespace etc.
  • preferNameDirectory: This should generally be set to true for most templates. It determines if the directory name is used as the project name when a name is not specified.

That covers what is required in the template.json file to make a template and it this point we technically have a working template. You can install a template from a directory just the way we have it laid out, however the normal means of delivering a template is to package it up as a NuGet package.

Packaging the Template

To package up our template as a NuGet package for consumption we’ll need to add a nuspec file to the solution and then use the nuget pack command to package it up. To perform these steps you’ll need to have the NuGet cli installed and available on your path. (downloads | documentation)

You can generate a nuspec file and then modify it as needed by using the nuget spec command. (Or you can just copy the nuspec below and modify to fit your needs)

nuget spec DotNetNinja.Templates.Mvc

Here is my modified nuspec ready to go. (Note the template version is in this file. You will want to update that version with each release of your template!)

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
  <metadata>
    <id>DotNetNinja.Templates.Mvc</id>
    <version>1.0.0</version>
    <description>
      Template for creating an ASP.NET Core MVC Solution
    </description>
    <authors>Larry House</authors>
    <license type="expression">MIT</license>
    <packageTypes>
      <packageType name="Template" />
    </packageTypes>
  </metadata>
  <files>
    <file src="Content\**\*.*" exclude="Content\**\bin\**\*.*;Content\**\obj\**\*.*" target="Content" />
  </files>  
</package>

We will place this file in our outer src folder (at the same level as our content folder) so it is outside our template.

To package the template navigate to the root of your repository and run a nuget pack command pointed to your nuspec file.

nuget pack .\src\DotNetNinja.Templates.Mvc.nuspec -OutputDirectory C:\Packages\NuGet

Note: I have a local NuGet source set up on my development machine in C:\Packages\NuGet. While this is not strictly required (you can install the template by specifying that path to the package) it does make the workflow easier (and it is convenient for testing other nuget packages). To set this up create the folder you want to use (ex. C:\Packages\NuGet) and run the following command.

nuget source add -Name LocalPackages -Source C:\Packages\NuGet

Being the lazy type ( 🙂 ), I’ve added a PowerShell script named Build-Template.ps1 in the root of my solution and put the nuget pack command in there so I don’t have to type out the full command all the time.

Our package should look something like this:

At this point it is probably a good time to install and do a quick test run of our template and ensure that everything is in order.

Assuming you have published your package to a folder that is a nuget source (as mentioned above) simply run the the following to install the template.

dotnet new -i DotNetNinja.Templates.Mvc

Notice that the new template has the name, short name, language, and tags we specified in our config file earlier. Now let’s run the template and create a new project from it. (You’ll probably want to create a test directory somewhere, don’t run it inside your template. Doing that will make a mess. Trust me.)

dotnet new ninjamvc -n SampleWeb

Here is a look at the Solution Explorer of my generated solution in Visual Studio.

If you look through the project you’ll find that all instances of DotNetNinja.Template.Mvc have been replaced with the name we specified above (SampleWeb). That includes the project names, namespaces, folder names etc.

In the next post we’ll take a look extending the template with optional files.

Resources

Running a Multi-Node Kubernetes Cluster on Windows with Kind

There are lots of ways to run Kubernetes on your Windows development machine, for example minikube. Even Docker Desktop for Windows now ships with the ability to run a single node Kubernetes cluster, but the simplest and most flexible way I’ve found to run a multi-node Kubernetes cluster locally on Windows is using Kind or Kubernetes in Docker.

Note

All commands should be run from an elevated (administrator) powershell or command prompt
Also, commands listed are for powershell. If using a command prompt you may need to make the appropriate modifications to the paths etc.

Prerequisites

In order to run Kind locally you’ll need to have or install Docker Desktop for Windows (you’re going to be running the nodes of your cluster in docker containers), kubectl (the Kubernetes command line utility), and of course kind. The easiest way to install them is using the Chocolatey package manager. (Strictly speaking you don’t have to have kubectl to set up a kind cluster, but it will make the cluster far more useful once you have it up and running! 🙂 )

choco install docker-desktop
choco install kubernetes-cli
choco install kind

If you would prefer not to use chocolatey, please see the installation documentation for each component

Creating Your First Cluster

Once everything is installed you’re ready to create your first cluster.

kind create cluster 

Note this will download a decent sized docker image to run your node(s) and may take a couple of minutes if you have a slower connection, but you should only have to do it once for each version of Kubernetes that you run as the image will be cached by Docker.

This gives us a single node cluster running the latest version of Kubernetes (at the time of this writing that’s 1.20). It also gives our cluster the default name of kind. You can control the version of Kubernetes by specifying the image to use for your nodes using the –image switch on your create cluster command.

kind create cluster --image=kindest/node:v1.19.7@sha256:a70639454e97a4b733f9d9b67e12c01f6b0297449d5b9cbbef87473458e26dca

You can get a list of the images available for a particular kind release on the Kind GitHub Releases page. For example the current release of Kind (0.10.0) supports the following images:

1.20: 
kindest/node:v1.20.2@sha256:8f7ea6e7642c0da54f04a7ee10431549c0257315b3a634f6ef2fecaaedb19bab
1.19: 
kindest/node:v1.19.7@sha256:a70639454e97a4b733f9d9b67e12c01f6b0297449d5b9cbbef87473458e26dca
1.18: 
kindest/node:v1.18.15@sha256:5c1b980c4d0e0e8e7eb9f36f7df525d079a96169c8a8f20d8bd108c0d0889cc4
1.17: 
kindest/node:v1.17.17@sha256:7b6369d27eee99c7a85c48ffd60e11412dc3f373658bc59b7f4d530b7056823e
1.16: 
kindest/node:v1.16.15@sha256:c10a63a5bda231c0a379bf91aebf8ad3c79146daca59db816fb963f731852a99
1.15: 
kindest/node:v1.15.12@sha256:67181f94f0b3072fb56509107b380e38c55e23bf60e6f052fbd8052d26052fb5
1.14: 
kindest/node:v1.14.10@sha256:3fbed72bcac108055e46e7b4091eb6858ad628ec51bf693c21f5ec34578f6180

You can also set the name of your cluster using the –name switch

kind create cluster --name my-cluster

Removing your Cluster

Eventually you will want to delete/remove/destroy your cluster and reclaim the resources it is using. You can easily accomplish this using the kind delete command.

kind delete --name my-cluster

Going to Multiple-Nodes

So initially I said that we could set up a multi-node cluster using kind, but so far all of our clusters have been a single node. In order to get a multi-node cluster we’ll need to create a configuration file. Here’s a simple example that creates a 3 node cluster (1 control plane node and 2 worker nodes)

# three node (two workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker

We can apply our config file by using the –config switch with our create cluster command as follows

kind create cluster --name my-cluster --config .\my-cluster-config.yaml

And there we have it, a multi-node Kubernetes cluster running on your Windows desktop. Kind makes it pretty quick and simple so you can spin one up, play with it while doing some training or deploy test workloads locally to test your entire system locally. Once your done, or you mess things up, delete the cluster and spin up a new one when you need it.

Resources

Adjusting Resources for Docker Desktop for Windows from PowerShell

Last week I was searching high and low for documentation of any kind on how to script a change in memory allocated to Docker Desktop for Windows. Unable to find anything online, and failing in all my attempts to piece together a way to make it happen, I opened an issue on GitHub and asked for advice. The fine folks in the Docker Desktop for Windows community on GitHub jumped in quickly to assist (Thank you again!). The suggestion was to change the Docker Desktop settings file directly. From that I was able to put together a process that works.

First we need to stop the docker services, there are two of them: com.docker.service and docker. To stop those is pretty straight forward.

Stop-Service com.docker.service
Stop-Service docker

or shorter:

Stop-Service *docker*

Next we’ll need to read the settings file which is located @ ~\AppData\Roaming\Docker\settings.json. We’ll want to use the $env:APPDATA variable to get the actual path to the ~\AppData\Roaming folder on the current system for the current user, and we’ll pipe the file contents to ConvertFrom-Json to give us a nice object to work with.

$path = "$env:APPDATA\Docker\settings.json"
$settings = Get-Content $path | ConvertFrom-Json

Now we can easily change the memory value by manipulating the memoryMIB property of our settings object.

$settings.memoryMiB = 4096

Then we can save the file again by piping our settings object to ConvertTo-Json and then to Set-Content.

$settings | ConvertTo-Json | Set-Content $path

Now we just need to restart the docker services.

Start-Service *docker*    

There’s one last crucial step. It turns out we need to give the Docker daemon a little nudge to get things responding to our docker commands again. According to this article on stack-overflow we do that using:

& $Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchDaemon
& $Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchDaemon

Yes, twice. I later derived by looking at the DockerCli help that I could just use:

&$Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchLinuxEngine

(I’m running Linux containers. If you are running Windows containers use -SwitchWindowsEngine instead.)

So here is the whole thing all in one go.

Stop-Service *docker*
$path = "$env:APPDATA\Docker\settings.json"
$settings = Get-Content $path | ConvertFrom-Json
$settings.memoryMiB = 4096
$settings | ConvertTo-Json | Set-Content $path
Start-Service *docker*        
&$Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchLinuxEngine

Resources

Creating a PowerShell Cmdlet in C#

Creating a PowerShell Cmdlet in C# is actually a fairly straight forward endeavor. At it’s core you simply create a new class that derives from one of two base classes (Cmdlet or PsCmdlet), add properties to the class to accept your parameters, override one or more methods in the base class to provide your functionality, and decorate the class and properties with a few attributes. In this “How To” article I’m going to create a PowerShell Cmdlet that will add/update my hosts file with host names and IP addresses for each of the ingresses that I have configured in my local Kubernetes cluster running on minikube.

Choosing a Cmdlet Name

In PowerShell Cmdlets are named using a Verb-Noun format. For example Get-ChildItem, or Compress-Archive. Not only is this convention, but PowerShell will yell at you if you don’t follow the convention and use one of the “pre-approved” verbs.

WARNING: The names of some imported commands from the module 'Sample' include unapproved verbs that might make them less discoverable. To find the commands with unapproved verbs, run the Import-Module command again with the Verbose parameter. For a list of approved verbs, type Get-Verb.

If you choose to use a unapproved verb you (and anyone using your module) will be greeted with the big wall of yellow warning text above every time the module is loaded. So what are the approved verbs you can use?

Add, Approve, Assert, Backup, Block, Checkpoint, Clear, Close, Compare, Complete, Compress, Confirm, Connect, Convert, ConvertFrom, ConvertTo, Copy, Debug, Deny, Disable, Disconnect, Dismount, Edit, Enable, Enter, Exit, Expand, Export, Find, Format, Get, Grant, Group, Hide, Import, Initialize, Install, Invoke, Join, Limit, Lock, Measure, Merge, Mount, Move, New, Open, Optimize, Out, Ping, Pop, Protect, Publish, Push, Read, Receive, Redo, Register, Remove, Rename, Repair, Request, Reset, Resize, Resolve, Restart, Restore, Resume, Revoke, Save, Search, Select, Send, Set, Show, Skip, Split, Start, Step, Stop, Submit, Suspend, Switch, Sync, Test, Trace, Unblock, Undo, Uninstall, Unlock, Unprotect, Unpublish, Unregister, Update, Use, Wait, Watch, Write

To make things easier these are all defined in constants in the C# reference library. There are 7 classes with these constants: VerbsCommon, VerbsCommunications, VerbsData, VerbsDiagnostic, VerbsLifecycle, VerbsOther, and VerbsSecurity. Here’s the breakdown by class:

ClassNameVerbs
VerbsCommonAdd, Clear, Close, Copy, Enter, Exit, Find, Format, Get, Hide, Join, Lock, Move, New, Open, Optimize, Pop, Push, Redo, Remove, Rename, Reset, Resize, Search, Select, Set, Show, Skip, Split, Step, Switch, Undo, Unlock, Watch
VerbsCommunicationsConnect, Disconnect, Read, Receive, Send, Write
VerbsDataBackup, Checkpoint, Compare, Compress, Convert, ConvertFrom, ConvertTo, Dismount, Edit, Expand, Export, Group, Import, Initialize, Limit, Merge, Mount, Out, Publish, Restore, Save, Sync, Unpublish, Update
VerbsDiagnosticDebug, Measure, Ping, Repair, Resolve, Test, Trace
VerbsLifecycleApprove, Assert, Complete, Confirm, Deny, Disable, Enable, Install, Invoke, Register, Request, Restart, Resume, Start, Stop, Submit, Suspend, Uninstall, Unregister, Wait
VerbsOtherUse
VerbsSecurityBlock, Grant, Protect, Revoke, Unblock, Unprotect

In the case of my demo project, I’ll be updating the hosts file with the IP addresses of my ingresses in kubernetes, so the “Update” verb seems appropriate here. The noun part of the name is much simpler in that there are really now rules other than it should be a noun. In the case of the demo project I’ve chosen to name it “Update-HostsForIngress”.

Getting Started

Now that we have selected a name for our Cmdlet, let’s get started writing some code! Create a new .Net Framework Class Library project in Visual Studio to contain your Cmdlet. Mine is called DotNetNinja.KubernetesModule and I’m targeting .Net Framework 4.7.2.

Next you’ll need to add a NuGet Package reference to one of the Microsoft.PowerShell.X.ReferenceAssemblies packages depending on which version of PowerShell you want to target. I’ve selected the package Microsoft.PowerShell.5.ReferenceAssemblies because I’m targeting PowerShell 5, but there are packages for PowerShell 3, 4 and 5 available.

Now lets start by adding a class for our Cmdlet named UpdateHostsForIngressCmdlet. (You can name this class anything you want, PowerShell will look at the attributes we’ll be adding shortly to determine the actual Cmdlet name, but it is useful to have the name be easy to tell what Cmdlet it is for, especially when you start building up a library of Cmdlets in one project!). This name follows my personal naming convention for Cmdlet classes which is [Verb][Noun]Cmdlet. Basically take the name of the Cmdlet, remove the hyphen, and append Cmdlet to the end. Here’s the starting boiler-plate code for our Cmdlet class:

    public class UpdateHostsForIngressCmdLet: Cmdlet
    {
        public UpdateHostsForIngressCmdLet()
        {
        }

        public UpdateHostsForIngressCmdLet(IHostsFile hosts, IKubernetesCluster cluster)
        {
        }
        
        protected internal IHostsFile Hosts { get; private set; }

        protected internal  IKubernetesCluster Cluster { get; private set; }
        
        public string[] HostNames { get; set; }

        protected override void BeginProcessing()
        {
        }

        protected override void ProcessRecord()
        {
        }

        protected override void EndProcessing()
        {
        }

        protected override void StopProcessing()
        {
        }
    }

We have a class named UpdateHostsForIngressCmdlet that derives from Cmdlet in System.Management.Automation. (This is the assembly that was added when we added our package reference earlier.) We could also have derived from PsCmdlet, but we don’t need any of the enhanced features of that class (PsCmdlet itself derives from Cmdlet). The biggest use case I have seen for deriving from PsCmdlet is to get access to session storage that allows you to persist info across invocations of your Cmdlet. We won’t need that here, so we’ll just stick with Cmdlet. In order to tell PowerShell that this is a Cmdlet and what it’s name is we need to add a Cmdlet Attribute to the class, specifying two parameters, one for the verb portion of the name and one for the noun portion of the name:

    [Cmdlet(VerbsData.Update, Nouns.HostsForIngress)]
    public class UpdateHostsForIngressCmdLet: Cmdlet
    {
        ...

In the example above I’ve used constants for the names (The built in VerbsData.Update and a custom Nouns.HostsForIngress), but you can just use strings literals if you prefer though I do recommend at least using the built in verbs constants as it prevents you from using an unapproved verb. In addition to the Cmdlet attribute, we should also tell PowerShell about the types we will be outputting from our Cmdlet. We do this by adding an OutputType attribute to the class.

    [Cmdlet(VerbsData.Update, Nouns.HostsForIngress)]
    [OutputType(typeof(HostsUpdateResult))]
    public class UpdateHostsForIngressCmdLet: Cmdlet
    {
        ...

In our class we have two constructors, one default/empty constructor that PowerShell will use to instantiate our class and one that takes the two dependencies we have a parameters. The two dependencies are IHostsfile (which we will use to load and save our hosts file and to manage merging entries for our ingresses) and IKubernetesCluster (which we will use to communicate with our kubernetes cluster to get the information we need about our configured ingresses). Either way the class is constructed/instantiated, we’ll need to make sure the properties Hosts & Cluster get initialized so we can use them later.

    [Cmdlet(VerbsData.Update, Nouns.HostsForIngress)]
    [OutputType(typeof(HostsUpdateResult))]
    public class UpdateHostsForIngressCmdLet: Cmdlet
    {
        public UpdateHostsForIngressCmdLet()
        {
            var services = new ServiceLocator();
            Hosts = services.Get<IHostsFile>();
            Cluster = services.Get<IKubernetesCluster>();
        }

        public UpdateHostsForIngressCmdLet(IHostsFile hosts, IKubernetesCluster cluster)
        {
            Guard.IsNotNull(hosts, nameof(hosts));
            Guard.IsNotNull(cluster, nameof(cluster));
            Hosts = hosts;
            Cluster = cluster;
        }
        ...

In the first constructor I’ve used a ServiceLocator (built on top of Autofac) to instantiate my services. In the second constructor I’ve simply validated the passed in parameters are not null and initialized the service properties with them. This pattern keeps things loosely coupled and should make testing and modification/maintenance easier.

In addition to the two protected properties for our dependencies, we also have a string[] property called HostNames. We’ll use this (optionally if the value is provided) to filter the ingresses we are updating in our hosts file. To tell PowerShell about our parameter property we need to add another attribute, the Parameter Attribute, to the property declaration like so:

        [Parameter(Mandatory = false, Position = 0, ValueFromPipeline = true)]
        public string[] HostNames { get; set; }

Technically all that is required is the attribute itself [Parameter], but I’ve added Mandatory=false so that the parameter isn’t required (if not passed we’ll process all of the ingresses reported by Kubernetes), Position=0 so that our Cmdlet can be invoked with a parameter, but without specifying the name (The first/0th parameter will be mapped to our property), and ValueFromPipeline=true so that we can pipeline in the HostNames if we wish (maybe we’ll want to read them from a file?). With this configuration here are the valid invocations of our command.

# Will update all reported ingresses in the hosts file
Update-HostsForIngress  

# Will update only the specified ingress (kiali.minikube.local) using positional parameter mapping  
Update-HostsForIngress kiali.minikube.local

# Will update only the specified ingress (kiali.minikube.local) using named parameter mapping  
Update-HostsForIngress -HostNames kiali.minikube.local

# Will update all the specified ingresses using positional parameter mapping  
Update-HostsForIngress kiali.minikube.local, dashboard.minikube.local

# Will update all the specified ingresses using named parameter mapping  
Update-HostsForIngress -HostNames kiali.minikube.local, dashboard.minikube.local

# Will update all the specified ingresses using the pipeline as a parameter source  
"kiali.minikube.local", "dashboard.minikube.local" | Update-HostsForIngress

# Alternatively, same as above using a file as the source of the host names
Get-Content .\hostnames.txt | Update-HostsForIngress

Interacting With the PowerShell Session

In order to communicate/output information there are a number of “Write” methods on the Cmdlet base class:

WriteCommandDetailWrites information to the logs.
WriteDebugWrites information out to the debug stream. This is only visible if the -Debug flag is used.
WriteErrorWrites error information.
WriteInformationWrites informational messages out for user consumption.
WriteObjectWrites data objects out to the pipeline stream.
WriteProgressWrites progress information
WriteVerboseWrites writes information to the Verbose stream, This is only visible if -Verbose flag is used.
WriteWarningWrites warning messages for user consumptions. Typically shows as yellow text.

In our Cmdlet we’ll use WriteObject to pass our result objects back to the pipeline. Typically these will just be dumped to the console, but they could be pipelined into another Cmdlet as well. We’ll also use WriteWarning in a couple of cases and we’ll use WriteError in the case that kubectl is not found on the path.

Cmdlet Lifecycle

All that’s left from our initial stubbed out class is the four methods BeginProcessing, ProcessRecord, EndProcessing, StopProcessing. These four methods form the lifecycle of our Cmdlet. Here’s how they work.

BeginProcessing()

This methods is called once per invocation of our Cmdlet. It is intended to be used to initialize things in your Cmdlet like connections, or reading source files etc. We’ll use this method to load our Hosts file.

        protected override void BeginProcessing()
        {
            Hosts.Load();
        }

ProcessRecord()

This method is called once per item being processed. For example if you are pipelining data to your Cmdlet, it will be called once for each item passed by the pipeline. This is typically where you do the bulk of your work in your Cmdlet. In ProcessRecord we will connect to our Kubernetes cluster and get a list of all the currently configured ingresses and upsert the hostnames/IP addresses to our hosts file, merging them with any existing entries.

protected override void ProcessRecord()
        {
            try
            {
                var responses = Cluster.GetIngresses().ToList(); // Get all configured ingresses

                if (responses.Any()) // Only process if kubernetes returns a response
                {
                    // Grab the valid ingress entries for our purposes (only port 80/443 & not the header info)
                    var ingresses = responses.GetValidIngresses().ToList(); 
                    // Filter the ingresses by hostnames if they have been supplied
                    if (HostNames != null && HostNames.Any()) 
                    {
                        ingresses = ingresses.Where(ingress => HostNames.Contains(ingress.Hosts)).ToList();
                    }
                    
                    // Upsert the entries to the hosts file and capture the results for output
                    var results = ingresses.Select(ingress =>
                            Hosts.Upsert(ingress.Address, ingress.Hosts, $" Name: {ingress.Name} Namespace: {ingress.Namespace} EntryDate:{DateTime.Now}"))
                        .OrderBy(result => result.Status)
                        .ThenBy(result => result.HostName);
                    // Output each result as a separate object to allow better pipelining to other Cmdlets
                    foreach (var result in results)
                    {
                        WriteObject(result);
                    }
                    return;
                }
                // Warn the user if Kubernetes did not respond
                WriteWarning("No response was received from kubernetes.  Is your cluster running? (If running minikube locally, try 'minikube status' and/or 'minikube start')");
            }
            catch (Win32Exception ex)
            {
                // Handle the case when kubectl is not installed or is not on the path by showing a warning/error
                if (ex.Message.Contains("The system cannot find the file specified."))
                {
                    WriteWarning("The kubectl command was not found on your system. (Is it installed and on your path?)");
                    WriteError(new ErrorRecord(ex, Errors.KubectlNotFound, ErrorCategory.ResourceUnavailable, Cluster));
                    return;
                }
                // Re-throw any unknown errors
                throw;
            }
        }

EndProcessing()

EndProcessing as you may have guessed is called once processing is completed. This method is also called only once per invocation and is a place to run any clean up code and finalize things before exiting your Cmdlet. We’ll save any pending changes from EndProcessing.

        protected override void EndProcessing()
        {
            Hosts.Save();
        }

StopProcessing()

StopProcessing is called when your Cmdlet terminates prematurely. For example if you start the Cmdlet and then it [ctrl]-[c] then StopProcessing will fire giving you a chance to clean up anything you need to before the Cmdlet exits. We don’t have anything really useful to put in here for our Cmdlet.

Now, in theory, we have a working Cmdlet!

Debugging Your CmdLet

I’ve seen a couple of different ways people have proposed to debug Cmdlets written in C#. Most of them involve a lot of hoop jumping and attaching to processes manually from Visual Studio. By far the best, IMHO, is to configure your project in Visual Studio to debug by launching an external program (powershell.exe) and to pass it a set of command line parameters which among other things automatically loads your module. To do this, in Visual Studio right click on your project and select Properties. In the properties window go to the debug tab and set the following:

Start external program = C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Command line arguments = -NoProfile -NoExit -Command “Import-Module .\DotNetNinja.KubernetesModule.dll”

Your PowerShell path may vary on your system depending on configuration. Notice that I’m pointed to a v1.0 directory? Apparently v1.0 doesn’t mean what you’d think it means. This is the correct path for my local PowerShell 5 install and as far as I have seen this is a pretty standard path unless you made some explicit choices during system set up.

Now if you click the run button or press f5 a PowerShell window should open and your Cmdlet Module should already be loaded. Just set a break-point and invoke your command to debug.

Importing Your Cmdlet

There are a number of ways you can import your custom Cmdlet. The easiest way is just to import the dll directly from the command prompt. Just type Import-Module and the path to your dll.

Import-Module .\DotNetNinja.KubernetesModule.dll

This works fine if the Cmdlet is just for your own personal consumption and you don’t use it very often.

Another way to make it more easily available is to put it a location on your PowerShell Modules Path. You can see all the locations on your path using:

$env:PSModulePath

Typically there is a user specific path @ ~\Documents\WindowsPowerShell\Modules. Create a folder in the modules folder with the same name as your module and place your dll inside there. Now you can load it without specifying the path and it is discoverable using the Get-Module -ListAvailable command.

Using Module Manifest Files

To make your module available without specifying the dll you can create a Module Manifest file (a .psd1 file). The best way to scaffold out a manifest file is to use the New-ModuleManifest Cmdlet. In our case I’ve created one using:

New-ModuleManifest DotNetNinja.KubernetesModule.psd1

By using this command it will automatically generate a unique guid for your module. If you choose to use another means (like copying an existing file), be sure to generate a new guid for for module! Also be sure to set the RootModule so that PowerShell knows what to load. Here is a stripped down version of the one for my module.

#
# Module manifest for module 'DotNetNinja.Kubernetes'
#
# Generated by: DotNetNinja
#
# Generated on: 2020-02-29
#

@{

RootModule = 'DotNetNinja.KubernetesModule.dll'
ModuleVersion = '1.0.0'
GUID = '9b2f7509-1bac-4ea6-8211-798a920baa2c'
Author = 'Larry House'
Copyright = '(c) Larry House. All rights reserved.'
Description = 'PowerShell Cmdlets for managing Kubernetes environments'
PowerShellVersion = '5.0'
CLRVersion = '4.0'
ProcessorArchitecture = 'Amd64'
FunctionsToExport = '*'
CmdletsToExport = '*'
VariablesToExport = '*'
AliasesToExport = '*'
PrivateData = @{

    PSData = @{


    } # End of PSData hashtable

} # End of PrivateData hashtable

}

Source Code

You can get the full source code from GitHub.

git clone https://github.com/DotNet-Ninja/DotNetNinja.PowerShellModules.git

Resources