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:

  1. Return valid ModuleCatalogEntry records from ListAsync.
  2. Set IsVerified = true only if you have verified the cosign signature against a trusted key.
  3. Return a readable stream from DownloadAsync that is a valid OCI layer tar archive.