Skip to content

Update sample for new Extension Loader system #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 22 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ Typical use cases would be triggering external business workflow systems on succ
This repository contains a sample job completion handler that traces out the job context information provided and
demonstrates making an API call back into the Command platform.

Job Completion Handlers were introduced in Command version 9.0. This sample has been tested with Command Version 10.3
Job Completion Handlers were introduced in Command version 9.0. This sample has been tested with Command Version 11.2.
The way Completion Handlers and other extensions are loaded in Command changed in version 11.
Please review the comments and examples in this repository in order to configure and load the Completion Handler correctly.


## Getting Started
Expand Down Expand Up @@ -88,47 +90,34 @@ The package repository is public read but does require a GitHub login.
Use Visual Studio to build the solution file in this repository.

Note that this extension will need to be compiled with the same version of the .NET Framework as is the Command
instance it will be plugged into. For Keyfactor Command 10.3 and prior, you will need to target .NET Framework 4.7.2
(which this solution does target).
Future versions of Command are planed to be targeted with .NET Core and if this sample is built against future versions,
the target framework may need to be updated.
instance it will be plugged into. This project targets both .NET 6.0 and .NET 8.0. The appropriate output can be selected for the Command version in use.
Future versions of Command are planed to be targeted for current versions of .NET.
If this sample or production Completion Handlers are built against future versions, the target framework may need to be updated.

#### 4 - Copy the assembly to the Command Server

Copy the compiled assembly (`SampleJobCompletionHandler.dll`) to bin folder for the Orchestrator API endpoint on the
Command Server. This is typically `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin `

For this sample only the above DLL needs to be copied to the target system.
Copy the compiled assembly (`SampleJobCompletionHandler.dll`), `manifest.json` and required dependencies to the correct location
for Extensions for the Orchestrator API endpoint on the Command Server. You may organize these under a single sub-directory.
This is typically `C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\JobCompleteHandlers`

In cases where your custom code may need additional dependent assemblies, make sure to only copy assemblies that are specific to your handler.
Do not overwrite DLLs that ship with the Command platform.
You will need to make sure that the handler references the same versions of libraries already in use in the WebAgentServices location.
As long as Extensions are copied into an isolated folder, there is no concern of overwriting libraries. Different versions of a library can be
used as long as they are included correctly in the Extensions folder in use.

#### 5 - Register the handler on the Command Server

Job completion handlers are registered via Unity.
Job completion handlers are loaded by the extension loader system using the `manifest.json` file.

Edit the web.config for the Orchestrator API endpoint on the Command Server. This is typically found at
`C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\web.config`
A sample `manifest.json` file for this extension is included in this repository. It should be edited with the correct Options values.
The correct job type GUIDs retrieved above should be entered in a comma-seperated list in the JobTypes property.

Add the following new registration inside of `<unity><container>` along with the other <register ... /> items.
Use the job type GUIDs from your environment instead of the ones below.
```
<register type="IOrchestratorJobCompleteHandler" mapTo="KFSample.SampleJobCompletionHandler, SampleJobCompletionHandler" name="SampleJobCompletionHandler">
<property name="JobTypes" value="49b3a17d-cada-4ec8-84c6-7719bf5beef3,4be14534-55b0-4cd7-9871-536b55b5e973,e868b3f8-9b6a-48b1-91c8-683d71d94f61" /> <!-- Comma separated list of Job Type GUIDs to process -->
<property name="FavoriteAnimal" value="Tiger" /> <!-- Sample parameter to pass into the handler. This parameter must be a public property on the class -->
</register>
```
Command ships with a `SendEmailOrchestratorJobCompleteHandler` that sends emails on job completion.
The registration for this handler is commented out, but is an easy thing to search for when locating where to
put the registration for this sample.

Multiple completion handlers may be registered, and the same handler may be registered multiple times
(presumably with different sets of JobTypes), but the Unity "name" value must be unique across all registrations.
Multiple completion handlers may be registered. If they are located in the same sub-directory for Extension loading,
the `manifest.json` file needs to specify __all__ extensions in a directory to load. If you do not wish to merge `manifest.json`
files, organize extensions into seperate sub-directories entirely.

Note that a broken Unity registration or a registration that points to an assembly that cannot be loaded can prevent
Note that an incorrect `manifest.json` file or a `manifest.json` that points to an assembly that cannot be loaded can prevent
the Orchestrator API from operating and prevent all Orchestrators from contacting the platform. Be sure the check the
logs (see below) for proper operation any time the Unity registration or the corresponding Assemblies are changed.
logs (see below) for proper operation any time the `manifest.json` or the corresponding Assemblies are changed.

#### 6 - Enable trace logging for the handler

Expand All @@ -147,7 +136,7 @@ just before the default logging rule:

#### 7 - Restart IIS

Changes to Unity registration will require that the web server is restarted, which can be done by running the iisreset command.
Changes to extensions and a `manifest.json` will require that the web server is restarted, which can be done by running the iisreset command.

At this point trace messages should appear in the Orchestrator API endpoint logs whenever a WinCert store type job
is completed by an orchestrator. By default the logs will be at `C:\Keyfactor\logs\Command_OrchestratorsAPI_Log.txt`
Expand Down Expand Up @@ -281,7 +270,7 @@ The context fields provided are:
|JobResult | Result reported by Orchestrator. (Success, Warning, Failure, Unknown) |
|JobId | These will be fixed for reoccurring jobs, such as inventory, and change for one time jobs |
|JobType | String based Job type |
|JobTypeId | GUID for job type. These will be unique per Command instance. These are the values that were configured in the JobTypes Unity configuration|
|JobTypeId | GUID for job type. These will be unique per Command instance. These are the values that were configured in the JobTypes Options |
|OperationType | Some job types have multiple operations. While inventory jobs report "Unknown", management jobs can report "Add" and "Remove" |
|CertificateID | The internal Command Id for the certificate related to the job. Useful for knowing which certificate was added, remove, or enrolled |
|RequestTimestamp | When the job was originally scheduled |
Expand Down Expand Up @@ -381,6 +370,6 @@ Client : https://command.kftrain.lab/KeyfactorAPI/
This document has provided an example Job Completion Handler for a specific job type and can be extended to handle other job types.
Information was provided for using the Keyfactor APIs to find the correct GUIDs to process specific job types.
The sample also provides examples for handling Inventory, Management and Reenrollment jobs, along with how to access the context properties and a working HTTP Client.
Once the handler has been developed, instructions were provided describing how to add the handler to the Unity container so Keyfactor Command can call the appropriate handler.
Once the handler has been developed, instructions were provided describing how to load the handler with a `manifest.json` file so Keyfactor Command can call the appropriate handler.
For additional information, please refer to the comments in the example source code.

36 changes: 0 additions & 36 deletions SampleJobCompletionHandler/Properties/AssemblyInfo.cs

This file was deleted.

86 changes: 55 additions & 31 deletions SampleJobCompletionHandler/SampleJobCompletionHandler.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
// Copyright 2023 Keyfactor
// Copyright 2025 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain a
// copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
Expand All @@ -16,6 +16,7 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;

// This is a sample implementation of the Keyfactor Command Orchestrator Job Completion Handler.
// This code can be run after either successful or failed completion of an orchestrator job and
Expand All @@ -37,19 +38,14 @@
// The TLDR; is that you need a working Keyfactor Command instance that is configured with scheduled
// inventory job on a WinCert store type. This assembly needs to be compiled and registered on the
// Command instance, and the deployment specific GUIDs for the WinCert store jobs need to be configured
// in the Unity registration.
// in the manifest.json file for the extension. A sample manifest.json is included in this repository.
//
// The compiled assembly SampleJobCompletionHandler.dll goes in C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\bin
//
// The web.config Orchestrator API normally at C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\web.config
// on the Command (IIS) server needs to have the following Unity registration added (with the job type GUIDs) specified.
//
/*
<register type="IOrchestratorJobCompleteHandler" mapTo="KFSample.SampleJobCompletionHandler, SampleJobCompletionHandler" name="SampleJobCompletionHandler">
<property name="JobTypes" value="" /> <!-- Comma separated list of Job Type GUIDs to process -->
<property name="FavoriteAnimal" value="Tiger" /> <!-- Sample parameter to pass into the handler. This parameter must be a public property on the class -->
</register>
*/
// The compiled assembly SampleJobCompletionHandler.dll goes in
// C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\Extensions\JobCompleteHandlers
// The manifest.json and all DLL dependencies should be included with the Completion Handler as well.
// To have the binary dependencies included in build output, the following line should be included in the .csproj file:
// <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
// An example can be seen in the SampleJobCompletionHandler.csproj for this project.
//
// Adding a rule to the NLog configuration which traces output from this code can be helpful. The NLog configuration
// is normally at C:\Program Files\Keyfactor\Keyfactor Platform\WebAgentServices\NLog_Orchestrators.config. The
Expand Down Expand Up @@ -84,27 +80,55 @@ public ILogger Logger

#endregion

#region Unity Properties
#region Handler Configuration Options and Parameters

// IMPORTANT: Public properties are no longer set automatically by dependency injection.
// The Options system needs to be used to pass in and use settings for extensions using a manifest.json.

// Parameters may be passed into the completion handler via Options in the manifest.json file.
// The Options are loaded in the constructor. Options might not be specified in the manifest.json, so the
// constructor should handle the fields appropriately. Options need to be accessed to be applied to
// public properties defined in this class.

// Typically these would be used to configure the behavior of the handler or other extension. These might be
// used to indicate if the logic in the handler should operate in production or test mode; or to configure
// external API addresses or credentials needed to talk to external systems.

// JobTypes is a required public property. It will contain a comma separated list of the job type GUIDs that
// that the handler should be prepared to handle. This value is set in the Unity configuration.
// that the handler should be prepared to handle. This value must be set by passed in Options in the manifest.json.
// Command will only call this handler when jobs of types in this list are complete.
// This sample doesn't make use of this property.
// This sample doesn't make use of this property directly, but it is used implicitly by the platform.

public string JobTypes { get; set; }

// Custom parameters may be passed into the completion handler via the Unity registration in the web.config
// file for the Orchestrator API endpoint. When the Unity Dependency Injection loads this class, it will apply
// values in the configuration to the class public properties.
//
// Typically these would be used to configure the behavior of the handler. These might be used to indicate
// if the logic in the handler should operate in production or test mode; or to configure external API
// addresses or credentials needed to talk to external systems.
//
// In this sample we will simply log out the parameter passed in.
// Custom options for configuration settings or other purposes can also be defined.
// In this sample an option with a string value is passed in and will be logged.

public string FavoriteAnimal { get; set; }

private readonly Options _options;

// IMPORTANT: The inferred type of the Options subclass here is critical for loading the IOptions<Options>
// object correctly. Creating the class Options as a public subclass is the most straightforward way to do this.
// If defining the Options subclass in a separate file, note that the full type
// MUST BE: <extension namespace>.<extension type>.Options to load properly.
public class Options
{
public string? JobTypes { get; set; }
public string? FavoriteAnimal { get; set; }
}

public SampleJobCompletionHandler(IOptions<Options> options)
{
_options = options.Value;
if (string.IsNullOrEmpty(_options.JobTypes))
{
throw new Exception("JobTypes must be specified in Options in order for this Completion Handler to run.");
}
JobTypes = _options.JobTypes;
FavoriteAnimal = string.IsNullOrEmpty(_options.FavoriteAnimal) ? "Unspecified" : _options.FavoriteAnimal;
}

#endregion

#region Hard Coded Job Configuration
Expand All @@ -126,7 +150,7 @@ public ILogger Logger

#region Handler Entry Point

// Whenever an orchestrator job completes whose JobType GUID is in the Unity configured list of GUIDs (JobTypes),
// Whenever an orchestrator job completes whose JobType GUID is in the JobTypes public property,
// the RunHandler method is called and passed a context object containing the details of the job that has
// completed. It is up to the code in the handler to perform whatever custom processing is desired.
//
Expand Down Expand Up @@ -154,9 +178,10 @@ private async Task<bool> AsyncRunHandler(OrchestratorJobCompleteHandlerContext c
return false;
}

Logger.LogTrace($"Entering Job Completion Handler for orchestrator [{context.AgentId}/{context.ClientMachine}] and JobType '{context.JobType}'");
Logger.LogTrace($"This handler's favorite animal is: {FavoriteAnimal}");
Logger.LogTrace($"The context passed is: \r\n[\r\n{ParseContext(context)}\r\n]");
Logger.LogInformation($"Entering Job Completion Handler for orchestrator [{context.AgentId}/{context.ClientMachine}] and JobType '{context.JobType}'");
Logger.LogInformation($"This handler's favorite animal is: {FavoriteAnimal}");
Logger.LogInformation($"The context passed is: \r\n[\r\n{ParseContext(context)}\r\n]");
Logger.LogInformation($"Version of Keyfactor.Logging: {typeof(LogHandler).Assembly.GetName().Version.ToString()}");

// Depending on the job type, call the appropriate handler. We switch on the job name constants
// define above.
Expand Down Expand Up @@ -208,8 +233,7 @@ private async Task<bool> InventoryHandler(OrchestratorJobCompleteHandlerContext
// the security context of Application Pool with which the Orchestrator endpoint is running as.
// Assuming that Windows Authentication is enabled on the endpoint (which is by default), and
// assuming that the App Pool account has been granted the appropriate API privileges, this allows
// easy calling of the API without having to manage a set of credentials in code or in the Unity
// configuration.
// easy calling of the API without having to manage a set of credentials in code or in the manifest.json.

// In this example we retrieve the job history for the job that just completed. We don't do anything
// with the results, but history could be examined to see if the job we repeatedly failing and then
Expand Down
Loading