Develop a CloudSmith module
CloudSmith modules are .NET assemblies that implement the ICloudSmithModule lifecycle interface. They are packaged as OCI artifacts, pushed to a GHCR registry, cosign-signed, and installed from the module catalog.
Prerequisites
- .NET 9 SDK
- Docker (for packaging)
cosign(for signing before publishing)- A GHCR repository under
ghcr.io/<your-org>/
ICloudSmithModule lifecycle
Every module must implement ICloudSmithModule:
using CloudSmith.Core.Modules;
public interface ICloudSmithModule
{
/// <summary>Module ID — must match the catalog entry (e.g., "monitoring").</summary>
string Id { get; }
/// <summary>Human-readable display name.</summary>
string Name { get; }
/// <summary>SemVer version string (e.g., "1.2.0").</summary>
string Version { get; }
/// <summary>
/// Called once when the module is loaded. Use this to validate configuration,
/// register background services, and perform any one-time setup.
/// Throw to abort module load and prevent the module from becoming active.
/// </summary>
Task InitializeAsync(ICloudSmithModuleContext context, CancellationToken cancellationToken);
/// <summary>
/// Called repeatedly on a platform-defined schedule (default: every 30 seconds).
/// Perform recurring work here — polling, health checks, data collection.
/// This method must return promptly; use background tasks for long-running work.
/// </summary>
Task ExecuteAsync(ICloudSmithModuleContext context, CancellationToken cancellationToken);
/// <summary>
/// Called when the module is unloaded (platform shutdown or explicit uninstall).
/// Release resources, cancel background tasks, flush buffers.
/// </summary>
ValueTask DisposeAsync();
}
ICloudSmithModuleContext
The ICloudSmithModuleContext is injected into InitializeAsync and ExecuteAsync. It provides access to platform services:
public interface ICloudSmithModuleContext
{
/// <summary>Logger scoped to this module.</summary>
ILogger Logger { get; }
/// <summary>Key/value configuration for the module (from appsettings or environment).</summary>
IConfiguration Configuration { get; }
/// <summary>
/// Access to registered clusters. Use to enumerate clusters, nodes, and run jobs.
/// </summary>
IClusterRepository Clusters { get; }
/// <summary>Job scheduler for submitting operations to cluster nodes.</summary>
IJobScheduler Jobs { get; }
/// <summary>Platform event bus — publish and subscribe to CloudSmith domain events.</summary>
IEventBus Events { get; }
/// <summary>
/// Persistent key/value store scoped to this module.
/// Use for state that must survive module restarts.
/// </summary>
IModuleStateStore State { get; }
}
ICloudSmithModuleCatalog
The ICloudSmithModuleCatalog interface represents the catalog of available modules. Implement this interface to provide a custom catalog source (for example, an internal GHCR mirror or a private artifact feed):
public interface ICloudSmithModuleCatalog
{
/// <summary>
/// List all modules available in this catalog source.
/// </summary>
Task<IReadOnlyList<ModuleCatalogEntry>> ListAsync(CancellationToken cancellationToken);
/// <summary>
/// Get a specific module by ID.
/// </summary>
Task<ModuleCatalogEntry?> GetAsync(string moduleId, CancellationToken cancellationToken);
/// <summary>
/// Download a module artifact for installation.
/// Return a stream containing the OCI layer tar archive.
/// </summary>
Task<Stream> DownloadAsync(string moduleId, string version, CancellationToken cancellationToken);
}
ModuleCatalogEntry
public record ModuleCatalogEntry(
string Id,
string Name,
string Version,
string Description,
string ImageReference, // e.g., "ghcr.io/cloudsmith-cloud/module-monitoring:1.2.0"
bool IsVerified // true if cosign signature is valid
);
Register the catalog with DI
Use the AddCloudSmithModuleCatalog() extension to register your catalog implementation:
// In Program.cs or a service registration extension
using CloudSmith.Core.Modules;
builder.Services.AddCloudSmithModuleCatalog<MyCustomModuleCatalog>();
For the built-in GHCR catalog, no explicit registration is needed — it is registered by default. Call AddCloudSmithModuleCatalog only when replacing or supplementing the default catalog.
Supplementing with a private catalog
builder.Services.AddCloudSmithModuleCatalog<GhcrModuleCatalog>(); // default
builder.Services.AddCloudSmithModuleCatalog<PrivateModuleCatalog>(); // appended
When multiple catalog sources are registered, GET /api/v1/modules/catalog returns the merged list. Module IDs must be unique across all sources; duplicates from later registrations are ignored.
Module implementation example
using CloudSmith.Core.Modules;
[CloudSmithModule]
public class InventoryCollectorModule : ICloudSmithModule
{
public string Id => "inventory-collector";
public string Name => "Inventory Collector";
public string Version => "1.0.0";
private PeriodicTimer? _timer;
public async Task InitializeAsync(ICloudSmithModuleContext context, CancellationToken cancellationToken)
{
context.Logger.LogInformation("Inventory Collector initializing.");
// Validate required config, register event subscriptions, etc.
await Task.CompletedTask;
}
public async Task ExecuteAsync(ICloudSmithModuleContext context, CancellationToken cancellationToken)
{
var clusters = await context.Clusters.ListAsync(cancellationToken);
foreach (var cluster in clusters)
{
var job = await context.Jobs.SubmitAsync(new JobRequest
{
ClusterId = cluster.Id,
Type = "InvokeScript",
Payload = new { Script = "Get-VM | Select-Object Name, State | ConvertTo-Json" }
}, cancellationToken);
context.Logger.LogInformation("Submitted inventory job {JobId} for cluster {ClusterId}",
job.Id, cluster.Id);
}
}
public ValueTask DisposeAsync()
{
_timer?.Dispose();
return ValueTask.CompletedTask;
}
}
Package as an OCI artifact
CloudSmith modules are distributed as OCI artifacts — Docker images containing only the module assembly and its dependencies.
Dockerfile
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
WORKDIR /module
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /module --no-self-contained
FROM base AS final
COPY --from=build /module .
# No ENTRYPOINT — the platform loads the assembly, not a standalone process
LABEL org.opencontainers.image.title="CloudSmith Inventory Collector"
LABEL org.opencontainers.image.version="1.0.0"
LABEL cloudsmith.module.id="inventory-collector"
Build and push
docker build -t ghcr.io/<your-org>/module-inventory-collector:1.0.0 .
docker push ghcr.io/<your-org>/module-inventory-collector:1.0.0
Sign the artifact with cosign
Modules installed from the catalog are signature-verified before loading. Sign your artifact before publishing it to users:
# Generate a key pair (first time only)
cosign generate-key-pair
# Sign the image
cosign sign --key cosign.key ghcr.io/<your-org>/module-inventory-collector:1.0.0
Publish cosign.pub alongside your module documentation so operators can verify signatures independently.
To install an unsigned module, the operator must explicitly acknowledge the warning in the portal or pass --allow-unverified to cs module install.
Publish to a custom catalog
If you host modules in a private GHCR registry or an internal artifact feed, implement ICloudSmithModuleCatalog to expose them in the CloudSmith catalog UI and CLI. Register the implementation with AddCloudSmithModuleCatalog<T>() in your platform’s appsettings.Local.json-driven DI configuration or via a platform extension assembly.
Your catalog implementation must:
- Return valid
ModuleCatalogEntryrecords fromListAsync. - Set
IsVerified = trueonly if you have verified the cosign signature against a trusted key. - Return a readable stream from
DownloadAsyncthat is a valid OCI layer tar archive.