-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Feature/auto setup v2 #8596
Feature/auto setup v2 #8596
Conversation
This feature always missing to be able to create multiple tenants. You can create one main tenant but can't add tenants inside that default tenant. Maybe it shouldn't, I don't know, but somehow if you want to automate the creation of OC tenants it would probably require to run recipes too on them afterward. |
@Skrypt I noticed there is a CreateTenantTask, Can the Setup recipe create a Workflow Task and setup another tenant(s) for now?
|
Added support for Multitenant setup I do not know why the build is failed:
I didn't touch SetupContext and it has a definition for AdminEmail and other properties |
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.
A quick look as I see the build failed.
The reason for the build failure is this pr was merged #7885
so the setup context has changed to use a properties dictionary.
It's interesting as a module, because then you might expect that you can continue to use it to setup tenants, once the default shell is running.
But you wouldn't be able to do that I suspect?
src/OrchardCore.Modules/OrchardCore.AutoSetup/AutoSetupMiddleware.cs
Outdated
Show resolved
Hide resolved
/// <returns> The <see cref="Action"/>. </returns> | ||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) | ||
{ | ||
var scopedServices = ShellScope.Services; |
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.
can you not inject these?
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 not sure, I saw it is being used in the other parts, @jtkech can you suggest, please?
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 question ;)
Yes it is used in some places, but because it is less DI friendly we could say it is better to use the DI when it is possible, we use it e.g. for ISiteService
that is a tenant singleton for some reasons but that needs to be executed in a tenant / shell scope context, this to resolve scoped services.
Here it's the same kind of thing, yes IStartupFilter
are executed under a shell scope context while building the tenant pipeline, but they are resolved through the tenant / shell container, not through a shell scope, we could have done it but we opted to keep for a tenant the pattern of IStartupFilter
as for a regular "app" (here the tenant as an app with its own shell container).
// Create a nested pipeline to configure the tenant middleware pipeline
var startupFilters = appBuilder.ApplicationServices.GetService<IEnumerable<IStartupFilter>>();
So here, if you inject IServiceProvider
it would be related to the tenant / shell container, not the one of the current shell scope, so you would not be able to resolve scoped services. So yes, using ShellScope.Services
is a solution, the other solution is to inject IHttpContextAccessor
to get the RequestServices
that we take care to be the services of the shell scope if there is a current one ;)
Both are using an AsyncLocal
object to be retrieved in a given async execution flow, the current HttpContext
or current ShellScope
. Personnaly i would use ShellScope
, but IHttpContextAccessor
is more aspnetcore friendly.
Hmm, maybe another solution through a module startup Configure()
that passes a scoped service provider, and with a lower ConfigureOrder
to get called earlier. Hmm, i could get an auto setup working by only using the config sources stack + a few lines of code #4567 (comment), but i don't remember what was missing.
i would need to take a deeper look on your PR, i will do soon when i will have time.
src/OrchardCore.Modules/OrchardCore.AutoSetup/Options/AutoSetupOptions.cs
Show resolved
Hide resolved
/// Gets or sets the Url which will trigger AutoSetup. | ||
/// Leave it Empty if you want to Trigger Setup on any request | ||
/// </summary> | ||
public string TriggerSetupUrl { get; set; } |
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.
public string TriggerSetupUrl { get; set; } | |
public string AutoSetupSetupUrl { get; set; } |
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.
Sorry personally double setup "SetupSetup" confusing me, but I'm ok with it if others don't mind
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.
Typo: suggest - AutoSetupPath
a. Trigger is weird
b. It's more a path than a url.
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.
Fixed
// "DatabaseTablePrefix": "", | ||
// "RecipeName": "Agency" | ||
// }, | ||
// "SubTenants": [ |
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 this could just be one array, where you find the Default
shell by it's name.
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 thought about this, then decided to split one explicitly since:
- It is not obvious, Not everyone knows the "Default" tenant name naming convention
- The Default Tenant does not need
RequestUrlHost
andRequestUrlPrefix
params
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 should use the default sections, including for other tenants.
I also think it should only initialize the tenant the first time it's requested, and not on the first request of the default one.
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.
@sebastienros sorry it is not clear to me, can you please rephrase your concern and/or give me an example?
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.
Not sure which part you didn't understand. What I mean is that last time I saw the code, the tenants were all initialized when the application is starting. It would be better if a tenant was auto-initialized on the first request to it. So if you open the default tenant, it's initialized, then if you open tenant1 it get initialized.
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've implemented on-demand initialization please take a look
Thank you
Right, you won't able to, since you enabling it as a Setup feature |
Fixes after code review
// "DatabaseTablePrefix": "", | ||
// "RecipeName": "Agency" | ||
// }, | ||
// "SubTenants": [ |
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 should use the default sections, including for other tenants.
I also think it should only initialize the tenant the first time it's requested, and not on the first request of the default one.
} | ||
} | ||
|
||
httpContext.Response.Redirect("/"); |
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.
~/ so it also works with the prefix of the default tenant
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.
Fixed
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.
Reverting it back, when I use redirection to ~/ it redirects me to https://localhost:5001/~/ 404
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, ~/
works when using Redirect (returning a RedirectResult) from a controller, because when executing the result the url helper.Content() is called and it uses the pathBase.
The pathBase contains an eventual virtual folder, and at this point it already contains the tenant prefix, the default tenant may have one prefix, so try the following
httpContext.Response.Redirect(httpContext.Request.PathBase)
var stringBuilder = new StringBuilder(); | ||
foreach (var error in setupContext.Errors) | ||
{ | ||
stringBuilder.Append($"{error.Key} : '{error.Value}'"); |
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.
AppendLine
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.
Fixed
stringBuilder.Append($"{error.Key} : '{error.Value}'"); | ||
} | ||
|
||
_logger.LogError($"AutoSetup failed installing the site '{setupOptions.SiteName}' with errors: {stringBuilder}"); |
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.
_logger.LogError($"AutoSetup failed installing the site '{setupOptions.SiteName}' with errors: {stringBuilder}"); | |
_logger.LogError("AutoSetup failed installing the site '{SiteName}' with errors: {Errors}", setupOptions.SiteName, stingBuilder); |
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.
Fixed
stringBuilder.Append(error.ErrorMessage); | ||
} | ||
|
||
_logger.LogError("AutoSetup did not start, configuration has following errors: {errors}", stringBuilder.ToString()); |
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.
_logger.LogError("AutoSetup did not start, configuration has following errors: {errors}", stringBuilder.ToString()); | |
_logger.LogError("AutoSetup did not start, configuration has following errors: {Errors}", stringBuilder.ToString()); |
/// <summary> | ||
/// The filter which registers <see cref="AutoSetupMiddleware"/> to setup the site. | ||
/// </summary> | ||
public class AutoSetupStartupFilter : IStartupFilter |
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 necessary since you could register the middleware from Configure in the same Startup class that registers this filter?
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 for review
I thought about this, I think I could do this by using middleware in the Startup. I guess the main difference is that ASP.NET Core uses IStartupFilters in order to build the application pipeline, by using IStartupFilter I can guarantee that AutosetupMiddleware will be the first in the pipeline. Please correct me if I'm wrong.
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, your startup filter will run first (app level ones still run first) as we call the startup filters of a given tenant (using its own app builder) before building its pipeline (so calling the modules startup Configure()
).
But for testing you can try to use a startup class, the one related to your auto setup feature, and override the ConfigureOrder
property with a negative value, e.g. -100
But just to test it because somewhere i like the idea of using a startup filter, the only thing is that the startup Configure()
provides the shell scope service provider, as commented in another comment, but for me not an issue by retrieving it with any other way
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 for the reviews, I replaced AutoSetupStartupFilter as a middleware in Statup.cs
I'd like to see if we can pass the arguments from the json file directly on the command line (since they are read the same way as from the ENV). |
Thanks for your review. Default Tenant
Single Sub Tenant
the 0 - Is an array index |
/// <param name="setupService"> The setup service. </param> | ||
/// <param name="shellSettings"> The tenant shell settings. </param> | ||
/// <returns> The <see cref="SetupContext"/>. to setup the site </returns> | ||
private static async Task<SetupContext> GetSetupContext(BaseTenantSetupOptions options, ISetupService setupService, ShellSettings shellSettings) |
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.
GetSetupContextAsync
/// <summary> | ||
/// The filter which registers <see cref="AutoSetupMiddleware"/> to setup the site. | ||
/// </summary> | ||
public class AutoSetupStartupFilter : IStartupFilter |
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, your startup filter will run first (app level ones still run first) as we call the startup filters of a given tenant (using its own app builder) before building its pipeline (so calling the modules startup Configure()
).
But for testing you can try to use a startup class, the one related to your auto setup feature, and override the ConfigureOrder
property with a negative value, e.g. -100
But just to test it because somewhere i like the idea of using a startup filter, the only thing is that the startup Configure()
provides the shell scope service provider, as commented in another comment, but for me not an issue by retrieving it with any other way
/// <returns> | ||
/// The <see cref="Task"/>. | ||
/// </returns> | ||
public async Task<bool> SetupTenant(ISetupService setupService, BaseTenantSetupOptions setupOptions, ShellSettings shellSettings) |
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.
SetupTenantAsync()
public async Task InvokeAsync(HttpContext httpContext) | ||
{ | ||
var currentShellSettings = ShellScope.Context.Settings; | ||
var setupService = httpContext.RequestServices.GetRequiredService<ISetupService>(); |
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.
Just to say that here we use the AsyncLocal ShellScope
to retrieve the settings, but not to resolve the service ;)
For me that's okay to use it or not for both, so just for infos the other way would be
var currentShellSettings = httpContext.RequestServices.GetRequiredService<ShellSettings>();
Just did a little review, will try to complete it asap, maybe this night. |
# Conflicts: # OrchardCore.sln
/// <summary> | ||
/// The default/root shell name. | ||
/// </summary> | ||
private const string DefaultShellName = "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.
Not needed, see below
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.
fixed
/// <summary> | ||
/// Gets the Flag which indicates a Default/Root shell/tenant. | ||
/// </summary> | ||
public bool IsDefault => string.Equals(ShellName, DefaultShellName, StringComparison.InvariantCultureIgnoreCase); |
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.
using OrchardCore.Environment.Shell;
string.Equals(ShellName, ShellHelper.DefaultShellName, StringComparison.OrdinalIgnoreCase);
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.
fixed
src/OrchardCore.Modules/OrchardCore.AutoSetup/Options/TenantSetupOptions.cs
Show resolved
Hide resolved
} | ||
} | ||
|
||
var redirectUrl = "/"; |
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.
Did you try my suggestion? The pathBase contains an eventual virtual folder, and at this point it already contains the tenant prefix (the default tenant may have one prefix).
httpContext.Response.Redirect(httpContext.Request.PathBase)
Or maybe
httpContext.Response.Redirect($"{context.Request.PathBase}/");
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.
Did you try my suggestion?
Yes, as you mentioned below context.Request.PathBase can be empty in this case I have to redirect to /.
Plus since we have AutoSetupOptions.AutoSetupPath
option the Request Url can be different from Tenant Url in this case I have to redirect to Tenant's Url
} | ||
|
||
httpContext.Response.Redirect(redirectUrl); | ||
await httpContext.Response.CompleteAsync(); |
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.
CompleteAsync()
: Is it necessary?
var logger = serviceProvider.GetRequiredService<ILogger<Startup>>(); | ||
|
||
|
||
if (currentShellSettings.State == Environment.Shell.Models.TenantState.Uninitialized) |
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.
Add an using so that you can just use TenantState.Uninitialized
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.
fixed
} | ||
} | ||
|
||
base.Configure(app, routes, serviceProvider); |
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.
You can remove this line
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.
fixed
IShellHost shellHost, | ||
ILogger<AutoSetupMiddleware> logger) | ||
{ | ||
_logger = logger; |
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.
Maybe put the logger at the end to keep the same order
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.
fixed
One of my concerns was to use or not pre-configured tenants, we can already do that by defining tenant sections, which are recognized as such if they contains a But also makes sense to have a separate section providing data that will only be used if the auto setup feaure is used, and that will override any pre-configured value, as done through the Tenants api controller. Note: But not for all properties in the Tenants mvc controller, e.g. if the database provider is already pre-configured, it is not shown on the setup screen. So, i'm not against and I will approve, but first i just did another little review. |
# Conflicts: # OrchardCore.sln
Do You mean defining the tenants section in App_Data tenants.json file? Cause this Autosetup feature will make sure and create other Tenants only if the Default tenant has successfully installed. |
stringBuilder.Append(error.ErrorMessage); | ||
} | ||
|
||
logger.LogError("AutoSetup did not start, configuration has following errors: {errors}", stringBuilder.ToString()); |
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.
Here, you are using localized strings for the validation, but then recording the validation errors in the log file, so it doesn't make sense that they are localized. Because log files don't contain localization.
If it was going to the front end, it would make sense for them to be localized, but when they are being appended to the log file, then it makes less sense. Because none of the ASP.Net errors, are localized, nor is the actual error message, of AutoSetup did not start
.
So having AutoSetup did not start : {errors in another language}
doesn't quite add up to me?
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 for your review, I agree with you, I've removed localization
|
||
if (currentShellSettings.State == TenantState.Uninitialized) | ||
{ | ||
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<AutoSetupOptions>>(); |
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.
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<AutoSetupOptions>>(); | |
var options = serviceProvider.GetRequiredService<IOptions<AutoSetupOptions>>().Value; |
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.
Fixed
No, in any regular config source as the A Default tenant always exists (even it is not pre-configured) at least in an Uninitialized state, and we can create and setup another tenant even if the default one is not setup, we currently do this in our unit tests. But that's okay, as said makes sense to have a separate and dedicated config section for all auto setup data, and then, because other tenants are not intended to be pre-configured (so not auto created by the shell settings manager), we need to use the default tenant to create them. I'm okay with this, but i think you got the idea ;) Note: But what happens if you create a tenant already pre-configured, hmm i think it will just override its settings
So use I'm trying it, before talking about url host and prefix, will see later, if you have a virtual folder, can be simulated by doing the following
Okay, you do a
The request Note: We have a
See in the Note: If the host is defined (not empty), we still need to take into account the original
Then about the prefix, regarding the code in the running shell table, if a host is defined for a tenant, this tenant can have a prefix or not, i tried it with a fake domain by using the So, aplying the same, here the code that i tried and that works if we use the above
Also, i had some But but but, if you define an url host for a given tenant (not the default one), because you pre-created it, the host is already in the ambient context (and also the url prefix), anyway you will not be able to trigger its setup if you don't use the url host that you defined for it. For the default tenant, hmm we fallback to it for any host, even if not yet defined, so if your running instance is already listening for this host, seems to be okay. If so, we would not need the above code, even for the default tenant, and the final code would be
Sorry for the long story, more line to describe what i was meaning, than the final code ;) What do you think about checking that the url prefix doesn't contain more than one segment, as done in the Finally, do a Update: Sometimes it show up that it is initializing, sometimes some exceptions, will see tomorow. |
First, i don't want to bother you with my suggestions and i don't want to block your PR ;) For now i only did minor changes, trying it revealed some little issues in other places that i'm fixing in separate PRs. As said above, sometimes the browser pre-sends some requests and then it just shows up that the tenant is initializing, normal and it doesn't prevent the setup to complete, so not a big issue but better to see the redirection operate.
Also related to multiple requests, sometimes i had yesSql / sqlite exceptions, here also it doesn't prevent the setup already started to complete. For now we only mark that the tenant is initializing, but not immediately and without any locking system. I already thought about this, now maybe worth to do something.
So, need a little more time to complete my tests and commit some suggestions, then you will be free to revert / update what you don't agree with. |
Sorry, did not have so much time, but just fixed the race conditions, i will commit my suggestions tomorrow |
@jtkech thanks for reviewing PR, I reworked the redirection logic by using |
The Second proposal of AutoSetup based on
#4567
Some Key Notes
services.AddOrchardCms().AddSetupFeatures("OrchardCore.AutoSetup");