-
Notifications
You must be signed in to change notification settings - Fork 759
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
[LocalDeploy] Migrate to extensibility v2 contract #14438
Conversation
Test this change out locally with the following install scripts (Action run 9843996390) VSCode
Azure CLI
|
string Message, | ||
string Target); | ||
ErrorDetail[]? Details, | ||
JsonObject? InnerError); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an expected contract for the InnerError
field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's defined by the OData 4.0 error response format: https://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091, which is used by the Graph API and some Azure data plane APIs. The deployment engine converts it to error.details[*].additionalInfo
to ensure compatibility with ARM RPC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks - so sounds like using JsonObject
rather than defining a custom record
for it is the way to go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup
{ | ||
case StringType: | ||
emitter.EmitProperty("type", "string"); | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to support the other built-in types as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, does config items will support all types - like in Template parameters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the runtime (deployment engine) supports all types except for user-defined types.
src/Bicep.Local.Deploy/Extensibility/LocalExtensibilityHandler.cs
Outdated
Show resolved
Hide resolved
|
||
namespace Bicep.Local.Deploy.Extensibility; | ||
|
||
public class AzExtensibilityExtension : LocalExtensibilityExtension |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extension suffix might be confusing as well, people might confuse with C# extension classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine, the convention for C# extension classes is to pluralize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although looking at the existing name which I picked, I'm wondering if we could call it something more indicative of what it's actually doing like NestedDeploymentExtension
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my other comment on LocalExtensibilityExtension.cs
- I'm learning more towards NestedDeploymentExtensibilityHost
as a name for this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it make more sense to make this a built-in local extension rather than an extension host?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NestedDeploymentBuiltInExtension
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed a bunch of deletions - packages.lock.json - I cleaned the solution and rebuilt but files are gone, doesn't seem expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would recommend just reverting this part of the change - the bicep-vs
solution should be unaffected. It's a separate .sln
file, so rebuilding the main solution file won't cause these files to be updated.
/// <summary> | ||
/// Enables bicep local deploy | ||
/// </summary> | ||
public bool LocalDeployEnabled { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is just a convenience property, and you don't want the value to differ from what is on the model, could we replace it with the following?
public bool LocalDeployEnabled => model.Features.LocalDeployEnabled;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved to use Context.SemanticModel.Features.LocalDeployEnabled
instead.
{ | ||
if (!providers.Any()) | ||
{ | ||
return; | ||
} | ||
|
||
// TODO: Remove if statement once all providers got migrated to extensions (extensibility v2 contract). | ||
if (this.Context.SemanticModel.Features.LocalDeployEnabled) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency, could we either access this property with Context.Settings.LocalDeployEnabled
everywhere (the convenience property you added), or with Context.SemanticModel.Features.LocalDeployEnabled
everywhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, I didn't know you get access to Settings or SemanticModel from Context. I'll remove the convenience property and use Context.SemanticModel.Features.LocalDeployEnabled
everywhere instead.
case StringType: | ||
emitter.EmitProperty("type", "string"); | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth handling secureString
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am avoiding it because secureString
requires bicep to read the value from KeyVault and not sure if all the wiring is in place right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after chatting with Shenglong, will be including the same types that we support today in Deployments
|
||
namespace Bicep.Local.Deploy.Extensibility; | ||
|
||
public abstract class LocalExtensibilityExtension : IAsyncDisposable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...ExtensibilityExtension
is a bit strange - what about LocalDeployExtension
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thoughts, I actually think it's fine to keep Provider
here (GrpcExtensibilityProvider
, NestedDeploymentExtensibilityProvider
). This is a different concept to the extension which a user would think about - the GrpcExtensibilityProvider
is a host for multiple user-defined extensions, rather than being an extension in itself. Maybe GrpcExtensibilityHost
/ LocalExtensibilityHost
would be a more appropriate name? @shenglol thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree that the class functions more as an extension host / dispatcher. I think either 'LocalExtensibilityHost' or 'LocalExtensionHost' would be suitable names.
/*var extensionName = requestUri.Segments[^4].TrimEnd('/'); | ||
var extensionVersion = requestUri.Segments[^3].TrimEnd('/'); | ||
var method = requestUri.Segments[^1].TrimEnd('/');*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Remove this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(facepalm) moved to copy the arg names and omit to remove these.
response.Headers.Add("Location", "local"); | ||
response.Headers.Add("Version", extensionVersion); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are these headers used for? Would you mind adding a code comment to explain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shenglol are we going to relax echo these header values in deployment engine? If so, these headers are just temporal additions and can add a comment but if is permanent, are these values ok?
To give you some context Anthony, deployment engine performs validations on header values and expects these two to be present, but it might not apply to local extensibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'll send a PR to relax the validation for the headers. We can remove them from LocalDeploymentEngineHost later.
return method switch | ||
{ | ||
"get" => await provider.Get(await content.ReadAsAsync<ResourceReferenceRequestBody>(cancellationToken), cancellationToken), | ||
"delete" => await provider.Delete(await content.ReadAsAsync<ResourceReferenceRequestBody >(cancellationToken), cancellationToken), | ||
"createOrUpdate" => await provider.CreateOrUpdate(await content.ReadAsAsync<ResourceRequestBody>(cancellationToken), cancellationToken), | ||
"preview" => await provider.Preview(await content.ReadAsAsync<ResourceRequestBody>(cancellationToken), cancellationToken), | ||
_ => throw new NotImplementedException($"Unsupported method {method}"), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels odd that this method deserializes the request (content.ReadAsAsync<ResourceReferenceRequestBody>
) but then returns an unserialized response (ResourceResponseBody
). Should we instead just have it return a HttpContent
or equivalent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also noticed that for response serialization you are using ResourceStack (.ToJson()
), but for request deserialization you are using the ASP.NET defaults (content.ReadAsAsync()
). This may yield different results.
My preference here would be to remove the dependency on HttpContent
and its built-in serializer at this layer, and instead take a Stream
for the body, and return a Stream
.
This removes the coupling with the HTTP layer, and puts us in full control the serialization + deserialization in one place (here).
@shenglol should we be using NewtonSoft or STJ for the classes under Azure.Deployments.Engine.Host.Azure.ExtensibilityV2.Contract.Models
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer using STJ for better performance. @jorgecotillo, we discussed using v2 models from Azure.Deployments.Extensibility instead. Could we switch to using those models?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
using Azure.Deployments.Extensibility.Contract; | ||
using Azure.Deployments.Extensibility.Messages; | ||
using Bicep.Core.Extensions; | ||
using Bicep.Core.Registry; | ||
using Bicep.Core.Semantics; | ||
using Bicep.Core.Semantics.Namespaces; | ||
using Bicep.Core.TypeSystem.Types; | ||
using Microsoft.WindowsAzure.ResourceStack.Common.Json; | ||
using Microsoft.WindowsAzure.ResourceStack.Common.Utilities; | ||
using IAsyncDisposable = System.IAsyncDisposable; | ||
|
||
namespace Bicep.Local.Deploy.Extensibility; | ||
|
||
public class LocalExtensibilityHandler : IAsyncDisposable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about ExtensionHostManager
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just thinking out loud here - what if we tweak the architecture? To me it seems a bit odd to have a manager or registry of extension hosts. Does it make sense to do the opposite? Instead of having a manager invoking an extension host, the LocalExtensionHost
could reference a LocalExtensionRegistry
. The LocalExtensionHost
would use the LocalExtensionRegistry
to resolve an extension based on a given extension name and version. Then, the LocalExtensionHost
would dispatch the resolved extension to handle resource operations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't block the PR, though. Since it's a design change, we may need to discuss it first and implement it later in subsequent PRs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, feels like there's definitely scope to simplify the design, and I like the above proposal. Working off it, some additional things to think about:
- Lifecycle management of
IExtension
instances - I assume we'd want this to be the responsibility of theExtensionRegistry
?- Activating extensions that aren't currently running (currently I believe this happens implicitly when an
IExtension
method call happens, but I think it should be more explicitly managed) - Deactivating unused extensions
- Keepalives (not currently implemented, but may need to be to avoid leaving orphaned processes running)
- Activating extensions that aren't currently running (currently I believe this happens implicitly when an
- Resolving extensions by
name
&version
leaves room for mixups to occur. I think we should key off the file system (cache) path to avoid this. This would need to be represented in the intermediate template somehow for the deployment layer to understand it.
handlerMock.SetupGet(x => x.ResourceType).Returns("apps/Deployment"); | ||
|
||
handlerMock.Setup(x => x.Save(It.IsAny<Protocol.ExtensibilityOperationRequest>(), It.IsAny<CancellationToken>())) | ||
.Returns<Protocol.ExtensibilityOperationRequest, CancellationToken>((req, _) => | ||
Task.FromResult(new Protocol.ExtensibilityOperationResponse(req.Resource, null, null))); | ||
handlerMock.Setup(x => x.CreateOrUpdate(It.IsAny<Protocol.ResourceRequestBody>(), It.IsAny<CancellationToken>())) | ||
.Returns<Protocol.ResourceRequestBody, CancellationToken>((req, _) => | ||
Task.FromResult(new Protocol.ResourceResponseBody(null, identifiers, req.Type, "Succeeded", req.Properties))); | ||
|
||
await RunExtensionTest( | ||
builder => builder.AddHandler(handlerMock.Object), | ||
async (client, token) => | ||
{ | ||
var request = new Extension.Rpc.ExtensibilityOperationRequest | ||
var request = new Extension.Rpc.ResourceRequestBody | ||
{ | ||
Import = new() | ||
{ | ||
Provider = "Kubernetes", | ||
Version = "1.0.0", | ||
Config = """ | ||
{ | ||
"kubeConfig": "redacted", | ||
"namespace": "default" | ||
} | ||
""" | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than deleting this, could we move it to the Config
property to be more representative of a real-life scenario?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added back
private Protocol.ResourceRequestBody Convert(ResourceRequestBody request) | ||
=> new(!string.IsNullOrEmpty(request.Config) ? JsonNode.Parse(request.Config) as JsonObject : null, | ||
request.Type, | ||
JsonNode.Parse(request.Properties)!.AsObject(), // TODO: Is there a better way to parse to ensure result might not be null? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a risk when parsing untrusted data, so it makes sense that we're forced to handle null
. My recommendation would be to just throw an exception if it happens.
I also think there's a lot going on in this function, so might be worth simplifying by using a traditional function rather than the expression body syntax:
private Protocol.ResourceRequestBody Convert(ResourceRequestBody request)
{
JsonObject? config = null;
if (request.Config is {})
{
config = JsonNode.Parse(request.Config)?.AsObject()
?? throw new ...;
}
var properties = JsonNode.Parse(request.Properties)?.AsObject()
?? throw new ...;
return new(config, request.Type, properties, request.ApiVersion);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was intended as a response to your comment // TODO: Is there a better way to parse to ensure result might not be null?
FYI
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, I updated the code and will push in the upcoming iteration
@@ -5,6 +5,7 @@ | |||
using System.Text.Json.Nodes; | |||
using Bicep.Local.Extension.Protocol; | |||
using Grpc.Core; | |||
using Grpc.Net.Client; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know why this was necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No clue, might be VS adding references when performing autocompletion, I just removed the unnecessary using statements btw.
src/Bicep.Local.Deploy/Extensibility/AzExtensibilityExtension.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
This reverts commit e280973.
Contributing a Pull Request
If you haven't already, read the full contribution guide. The guide may have changed since the last time you read it, so please double-check. Once you are done and ready to submit your PR, run through the relevant checklist below.
Contributing to documentation
Contributing an example
We are integrating the Bicep examples into the Azure QuickStart Templates. If you'd like to contribute new example
.bicep
files that showcase abilities of the language, please follow these instructions to add them directly there. We can still take bug reports and fixes for the existing examples for the time being.dotnet test
Contributing a feature
Contributing a snippet
I have a snippet that is either a single, generic resource or multi resource that uses parent-child syntax
I have checked that there is not an equivalent snippet already submitted
I have used camelCasing unless I have a justification to use another casing style
I have placeholders values that correspond to their property names (e.g.
dnsPrefix: 'dnsPrefix'
), unless it's a property that MUST be changed or parameterized in order to deploy. In that case, I use 'REQUIRED' e.g. keyDataI have my symbolic name as the first tab stop ($1) in the snippet. e.g. res-aks-cluster.bicep
I have a resource name property equal to "name"
If applicable, I have set the
location
property tolocation: /*${<id>:location}*/'location'
(notresourceGroup().location
) where<id>
is a placeholder id, and addedparam location string
to the test's main.bicep file so that the resulting main.combined.bicep file used in the tests compiles without errorsI have verified that the snippet deploys correctly when used in the context of an actual bicep file
e.g.
Microsoft Reviewers: Open in CodeFlow