Writing Coral Vendors

Vendors are essentially Coral plugins. They are implementations of Coral defined contracts that allow Coral to generically communicate with them. Vendors do not have direct access to Coral Core. The only communication between the Core and Vendors happens through the predefined contract.

As explained in the architecture document, Coral acts as an index of primitives (Resource, Group, Template), and vendors implement management of these primitives.

Extending Resources

This is the most common extension point of Coral. It allows Coral to orchestrate custom resources from any external service (e.g. ADO, Azure, github…).

When adding support for new resource type, one must put it in a namespace. Each namespace declares a configuration schema. A namespace configuration is made up of any number of resource configurations.

For each resource configuration, one must implement a resource actor. The responsibility of a resource actor is to use the provided configuration to create/delete/update a single resource. The state of that external resource is tracked by a model that the actor persists.

Extending Resources

Extending Groups

Group extensions allow for seamless integration of Coral UI to existing systems. Groups are used to provide context to resources, map them to existing external hierarchies, and delegate authorization to external systems.

For example, the Coral extension for ADO has a view that lists pipelines for a project. Coral doesn't have the concept of account, project or pipeline, but the Coral VSTS (ADO) vendor does. The ADO extension depends on the VSTS vendor to organize resources in a hierarchy that matches that of ADO.

It also uses the vendor to delegate authorization to it. For example, when a user wants to register a template in a VSTS project, Coral will check against the vsts vendor if (s)he has permission to do that. Internally, the vsts vendor will simply check if the user has admin permissions on the vsts project. This way, Coral avoids having to do redundant user, profile and access control management.

Extending Groups

Extending Templates

Templates are crucial for resource orchestration in Coral, but Coral does not store them in the core service. Instead, it uses vendors for that.

There are 2 components to implement when adding support for a new template store:

  • Template Provider: Used for template discovery (query/list available templates)
  • Template Actor: Reads the template and optionally subscribes to updates.

Extending Templates

Create a new local Vendor

Vendors may currently only be "in-process" which means that they are architecturally decoupled, but live in the same process as the core of Coral. In the future we aim to support vendors as separate services.

For example Vendors, look for already existing vendors in the Coral repo. Below, we will be using excerpts from the Example vendor.


When creating a new vendor, follow these steps:

  1. Create a new netstandard library project.
  2. Add a reference to the local vendor framework package in your csproj:
         <PackageReference Include="Microsoft.Coral.Vendor.Local.Framework" Version="$(CoralFrameworkVersion)" />
    
  3. Create a new C# class. It is recommended to add xml comments with a short summary. If present, this summary will be surfaced in the automatically generated documentation for your vendor.
     namespace Microsoft.Coral.Vendors.Example
     {
         using System;
         using Microsoft.Coral.Vendor.Local.Framework;
         using Microsoft.Coral.Vendor.Local.Framework.Routing;
         using Microsoft.Coral.Vendor.Local.Framework.State;
         using Microsoft.Coral.Vendors.Example.Actors;
         using Microsoft.Coral.Vendors.Example.Config;
         using Microsoft.Coral.Vendors.Example.Models;
    
         /// <summary>This is an example vendor.</summary>
         public class ExampleVendor : LocalVendorBase
         {
             public ExampleVendor(IStateManagerProvider stateManagerProvider, IResourceEventPublishChannelFactory resourceEventPublishChannelFactory)
                 : base(stateManagerProvider, resourceEventPublishChannelFactory)
             {
                 // Declare extensions...
             }
    
             // Used as a host in the vendor Uri (local://example)
             public override string Name => "example";
         }
     }
    
  4. Register the local vendor with Coral core runtime. Coral core runs in an Orleans silo. There are currently 2 silo launcher projects available. Look for WithVendors call which registers local vendors. Append WithLocalVendor<ExampleVendor>() to the delegate passed into that call. Do this in the following projects:
    1. Silo.Launcher in src/Orleans folder (Used for local development. Add your vendor here to local validation)
    2. SiloService in deployment folder (Used for deploying to Service Fabric. Add here when your vendor is ready for PPE/Prod)

The local vendors share a common dependency container. Feel free to add dependencies to it if needed by adding .WithDependency<TDependency>() call to the same delegate where the local vendors are registered (mentioned above).

Add a Namespace Provider

Even though local vendors run in the same process with the Coral service, they are referenced by Coral using URIs, just as if they were external services.

When extending any Coral primitive in a vendor, the URI needs to be declared for that extension so that Coral can interact with it. The LocalVendorBase provides the Router component that allows local vendors to assign URIs to all extensions including namespace providers.

To add a namespace provider to a vendor, add the following code to the Vendor constructor:

this.Router
    .AddStaticRoute("namespaces")
    .AddNamespaceProvider<ExampleNamespaceConfig>("v1", this.RegisterDefaultDependencies)
    .WithResource<ExampleConfig, ExampleActor, ExampleModel>(new Uri("https://icon.com/myicon.png"));

This registers a new namespace provider that works with ExampleNamespaceConfig to the URI local://example/namespaces/v1. It is usually a good idea to add the version number to the URI to better handle potential future breaking changes of the schema.

There is no need to implement the namespace provider itself. The local vendor framework will register the generic provider implementation. You can customize the dependency container for the resource actors by sending a custom dependency registration delegate to the AddNamespaceProvider method. If you do, please ensure it is calling this.RegisterDefaultDependencies() as well.

The registered provider has a single resource type declared, but more can be added by appending more WithResource calls. We haven't yet explained how to implement support for this resource type. This is explained in the next section.

Implement a Resource Actor

Here's an example resource actor implementation (explained in code comments)

namespace Microsoft.Coral.Vendors.Example.Resources
{
    using System;
    using System.Threading.Tasks;
    using Microsoft.Coral.Vendor.Local.Framework.Resources;
    using Microsoft.Coral.Vendor.Local.Framework.State;
    using Microsoft.Coral.Vendors.Example.Resources.Models;

    /// <summary>This is an example used by automatic documentation generation.</summary>
    public class ExampleActor : ResourceActorBase<ExampleConfig, ExampleModel>
    {
        private readonly IStateReference<ExampleModel> modelRef;

        public ExampleActor(ExampleConfig config, IStateReference<ExampleModel> modelRef)
            : base(config, modelRef)
        {
            this.modelRef = modelRef;
        }

        public override async Task CreateAsync()
        {
            // Read the injected config and create the concrete resource.

            // Once resource is created, build the model for it and store that model.
            // This will report creation success to Coral orchestrator
            await this.modelRef.CreateAsync(
                m =>
                {
                    m.Url = new Uri("http://example/resource");
                    m.Variable = "SomeVariable";
                });
        }

        public override async Task DeleteAsync()
        {
            // Load the stored model
            var model = await this.modelRef.ReadAsync();

            // Find the concrete resource using the model and delete it

            // Delete the model. This will report deletion success to Coral orchestrator
            await this.modelRef.DeleteAsync();
        }
    }
}

Add a parameter value provider

Just like with the namespace providers, to add a value provider, simply register the parameter value provider route in the vendor constructor:

var router = this.Router.AddStaticRoute("parameters");
router.AddValueProvider<ExampleSelectParameterValueProvider>("example-select", this.RegisterDefaultDependencies);

This example shows the registration of a basic select parameter value provider. Here's what the implementation of such provider might look like:

namespace Microsoft.Coral.Vendors.Example.Parameters
{
    using System.Threading.Tasks;
    using Microsoft.Coral.Vendor.Local.Framework.Parameters;
    using Microsoft.Coral.Vendor.Local.Framework.Parameters.Models;
    using Microsoft.Coral.Vendor.Parameters.Models;

    public sealed class ExampleSelectParameterValueProvider : SelectParameterValueProvider
    {
        public override Task<IParameterValueOption[]> SelectAsync()
        {
            return Task.FromResult(
                new IParameterValueOption[]
                {
                    new ParameterValueOption("Option1Value", "Display this text for option one"),
                    new ParameterValueOption("Option2Value", "Display this text for another")
                });
        }
    }
}

You can use one of the available base classes (like SelectParameterValueProvider above) to simplify the implementation, or you can implement one or more of the following interfaces directly:

  • IValidationProvider: Only implements input validation. All other parameter types extend from this.
  • IScalarValueProvider: There's a single provided value. This can be used to add non-interactive or hidden parameter inputs in the UX.
  • ISelectValueProvider: There's a moderately sized set of possible values the user must select from.
  • ISearchValueProvider: Could be an infinite set of possible values, and the user should be able to filter and select the one they need.

Add a Group Hierarchy

In the vendor constructor, register your group actors with any URI that makes sense for your vendor:

this.Router
    .AddStaticRoute("groups")
    .AddStaticRoute("super")
    .AddVariableRoute<IGroupActor, SuperGroupActor>("super", this.RegisteDependencies)
    .AddStaticRoute("sub")
    .AddVariableRoute<IGroupActor, SubGroupActor>("sub", this.RegisteDependencies)

Here's what an example group actor implementation might look like:

namespace Microsoft.Coral.Vendors.Example.Actors
{
    using System;
    using System.Threading.Tasks;
    using Microsoft.Coral.Vendor.Actors;

    public class SubGroupActor : IGroupActor
    {
        private readonly Uri groupUri;
        private readonly RequestHeaders requestHeaders;

        public SubGroupActor(Uri groupUri, RequestHeaders requestHeaders)
        {
            this.groupUri = groupUri;
            this.requestHeaders = requestHeaders;
        }

        // Return the parent group URI if any, null otherwise.
        // In this case, we know that to get the super group URI, we need to strip 2 path segments from the sub group URI.
        // In order to do this, we can use GetAncestor(2) extension method, or do string operations inline.
        public Task<Uri> GetAncestorAsync() =>
            Task.FromResult(this.groupUri.GetAncestor(2));

        // Used for access control. When a user operation against a group is first authorized, the session will be cached.
        // This method returns a context string which will be used to identify the cached authorization context.
        public override async Task<string> GetAuthenticationContextAsync() =>
            groupUri.ToString();

        // Used for access control. Here, we're assuming existence of RequestHeaders which contain an imaginary session token.
        // This is the case when the Coral client delegates the session token to coral through request context (e.g. HTTP headers).
        // Here, you can also implement any other delegated authorization flows that make sense for the vendor in question.
        public override Task<string> GetAuthenticationSessionAsync() =>
            Task.FromResult(this.requestHeaders.SessionToken);

        // Here, you can optionally modify the auth behavior by editing the GroupAuthenticationToken.
        // For example, you could set its InheritsPermissions parameter to true to inherit permissions granted to the user by the parent group context.
        // If you need to blindly delegate all permissions to the parent group, also ensure the context and session above return immutable static values.
        protected override Task<GroupAuthenticationToken> TransformAuthenticationTokenAsync(string contextToken, string sessionToken, GroupAuthenticationToken token) =>
            Task.FromResult(token);
    }
}