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.
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:
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:
<PackageReference Include="Microsoft.Coral.Vendor.Local.Framework" Version="$(CoralFrameworkVersion)" />
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";
}
}
WithVendors
call which registers local vendors. Append WithLocalVendor<ExampleVendor>()
to the delegate passed into that call. Do this in the following projects: 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).
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.
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();
}
}
}
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:
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);
}
}