Open tdhatcher opened 6 months ago
What are you doing for certificates in this scenario? Are you manually installing a self-signed cert for my.vanity-a.com
? Or are you just ignoring the cert warnings? Are you manually modifying your hosts file to make this work as well?
@mitchdenny I'd have to say all of the above. Which isn't too much overhead compared to needing clone repos, get secrets, etc. That initial ceremony but could mostly be scripted.
Manually doing the following.
At this point we probably aren't going to try to tackle this with Aspire but I'll leave this on the backlog to collect more feedback. I can see the utility of this but there are some inherent security risks of doing this.
@mitchdenny I'm not completely understanding if the security risk part is really relevant.
I think at this point the main thing I'm missing is just having the flexibility to insert an additional "aliased" endpoint link to render on the dashboard for the resource (and the verbosity to achieve that) to steer others who run the Aspire AppHost as configured. Otherwise I already have the behavior and flow working as I need it to. It's just that I have to communicate this in another way to others -- it's not intuitive to other devs off the shelf that it can be accessed in another way.
I did have the hostname and port I'm binding to whitelisted in the external Okta tool (or whatever configurable blackbox that is outside of the purview of Aspire)
Related to #1116
@DamianEdwards Thanks. Sorry I tried to find if there were any existing issues that were similar before posting. I'd say that #1116 is definitely on the same wavelength more than any other previous issues.
In my case 1) I would want the same port number (and specified) to serve for all hostnames. And 2) for it to be recognized on the Dashboard so a person would easily see "here are all the possible tenants/brands served for this resource". My application won't even function if I try to access it on localhost or 127.0.0.1 because it essentially will not map to a valid configuration state.
From some of what I have gathered service discovery seems to be something a bit more complex to solve for in general. For what it is worth, since I'm retrofitting existing applications into Aspire I'm basically crafting the environment variable for the service url configuration myself so I'm not really depedent on that at the moment. However my hope is to gradually migrate to adopt the Aspire conventions at some later time -- to remove most of the environment variable overrides.
I could live with this syntax of accessing some building blocks like this as it is more concise over what I have working now. I'll continue to experiement to see if I can come up with a cleaner workaround in the meantime.
var myUI = builder.AddProject<Projects.MyUI>("myUI")
// 7085 port is required
.WithEndpoint(port: 7085, scheme: "https", name: "vanitya", hostName: "my.vanity-a.com")
.WithEndpoint(port: 7085, scheme: "https", name: "vanityb", hostName: "my.vanity-b.com")
.WithEndpoint(port: 7085, scheme: "https", name: "vanityc", hostName: "my.vanity-c.com")
// Maybe another overload
.WithEndpoint(port: 7085, scheme: "https", hosts: [
new() {Name = "vanitya", hostName: "my.vanity-a.com"},
new() {Name = "vanityb", hostName: "my.vanity-b.com"},
new() {Name = "vanityc", hostName: "my.vanity-c.com"},
]);
;
var myServiceApi = builder.AddProject<Projects.MyServiceApi>("myServiceApi")
// Non-standard service url injection not following Aspire conventions
.WithEnvironment("Brands__A__Url", myUI.GetEndpoint("vanitya").Url)
.WithEnvironment("Brands__A__Url", "https://my.vanitya.com" + myUI.GetEndpoint("https").Port) // an less consise alternative
.WithEnvironment("Brands__B__Url", myUI.GetEndpoint("vanitya").Url)
.WithEnvironment("Brands__C__Url", myUI.GetEndpoint("vanitya").Url);
services__myui__vanitya__0
services__myui__vanityb__0
services__myui__vanityc__0
https+http://myui_vanitya/user
https+http://myui_vanityb/user
https+http://myui_vanityc/user
https+http://myui:vanitya/user
https+http://myui:vanityb/user
https+http://myui:vanityc/user
// This would look a bit weird (or astonishing).
vanitya://myui/user
vanityb://myui/user
vanityc://myui/user
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://*:{port doesn't matter to me}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
}
}
+1 & subscribed. I am also in the same boat.
I think that this is a super specific scenario so we'd be wanting to collect a lot more evidence around the need before we race off to implementation.
Supporting multi-tenant scenarios is challenging because there are so many different approaches and layers in which tenancy comes into play.
One idle thought about this. My inclination would be to define a reverse proxy as a resource within the app model which is configured to respond on all the domains you want and then pass the host header through to the ASP.NET project. This is more inline with how this kind of tenanting mechanism works in production anyway.
Then you could just have a single wildcard host entry for that endpoint. You'd probably need to handle the cert side of things yourself since we can only issue the standard ASP.NET Core dev cert at this point in time.
@mitchdenny I believe I understand what you are conveying here. It does feel like a heavier handed approach which is additional complexity just for Aspire purposes but not required with the existing application when launched locally with launchProfile with normal Visual Studio or even IIS. The equivalent was very easy to achieve with Tye, any binding added would just render the scheme and hostname/port on the dashboard.
If I'm not mistaken the reverse proxy appeoach still wouldn't help to address the primary "user friendliness" issue of allowing an uninitiated developer to easily discover the all the available hosted vanity urls by strictly relying on what is presented by the dashboard alone. How would the developer know which urls the reverse proxy resource supports?
The current missing vanity url details of a resource would have to be part of some buried and lesser obvious means such as exploring internal documentation or app configuration of the reverse proxy or the multi-tenant app as part of an onboarding process which is what I'm wanting to avoid (but currently appears to be my only option). Ideally, I would like the experience for other new devs on the team to be turnkey without having to dig into docs.
My actual use cases are driven by OIDC requirements and other multi-tenant/branded app behaviors that are already deployed in a production environment without the need for a reverse proxy. On here I've been relying on communicating similat usage scenarios that are contrived for simplification purposes but hopefully still accurate enough to communicate the desire.
Assuming no further work is done to make an easier or more convenient interface, at this point I'd say a workaround I'd really be interested in is how to influence what is rendered on the dashboard for all the endpoint links associated with a resource. That would do the trick. I'm just needing to inject a couple vanity urls to render for the resource.
I was able to sort of achieve a new entry using .WithAnnotation(new EndpointAnnotation(...))
however I am still in the same boat as changing localhost does not appear to be a parameter. Maybe I can extend that annotation and override it? I haven't dug deep enough there as to how the dashboard constructs the endpoint urls it renders.
But because I'm already using some of the hackiness around overriding context.EnvironmentVariables["ASPNETCORE_URLS"]
that at least does most of the heavy lifting to get the app to listen on host/port other than localhost. So if a developer is aware of the valid hosted vanity urls, they can still interact with the app in that context through thr browser or postman.
I can live with this little hackiness and extra configuration verbosity in the app host if it makes other devs on the project's life much easier. At least I'd have recipe to apply that makes the resource bind correctly and also render all its appropriate links on the dashboard.
+1 to the issue. This is currently blocking my environment.
@tdhatcher take a look at the CustomResource playground:
It uses ResourceNotificationService
to add update properties (in this case) on a resource. But you can also do things like add URLs. This is how the Azure provisioning logic works to add links to the deployment URLs in the Azure portal ... example:
It uses
ResourceNotificationService
to add update properties (in this case) on a resource. But you can also do things like add URLs.
Thanks @mitchdenny I will dig further into this to see what I can do with it. On vacation at the moment so not a laptop in sight for a couple weeks 😀.
Didn't mean to close this yet. Mobile!
This would also be useful for unix sockets, which we currently have no way of specifying the file path to the socket for an endpoint as well.
@glennc as I was talking to him about this recently
Didn't mean to close this yet. Mobile!
Are you planning to reopen?
Another thing this could help with is running out of space in localhost cookies with Aspire.
I now can't access Redis Commander, PG Admin, etc. because they throw HTTP 431 errors. Clearing cookies doesn't help because Aspire just puts them all back again on next run.
Being able to use different hostnames for services and projects would solve this issue. This is similar to feature request #5508 I opened earlier.
@hades200082 can you create a GitHub repo with a repro of the cookie issue you are seeing. This is the first I've seen of it. If you can reproduce it, then can you open a specific issue for that.
Will this also let me NOT set any host? When you specify a port binding, by default Docker will not assign a host IP:
docker run -it --rm -p 8001:8080 --name aspnetcore_sample mcr.microsoft.com/dotnet/samples:aspnetapp
"HostConfig": {
"NetworkMode": "bridge",
"PortBindings": {
"8080/tcp": [
{
"HostIp": "",
"HostPort": "8000"
}
]
},
It doesn't look like there are any options to NOT set the HostIp here except to skip the WithEndpoint
method. I have to do this:
var sample = builder.AddContainer("aspnetcore-sample", "mcr.microsoft.com/dotnet/samples", "aspnetapp")
//.WithEndpoint(8001, 8080, scheme: "http", isProxied: false, isExternal: true)
.WithContainerRuntimeArgs("-p", "8001:8080")
;
If I use WithEndpoint
then HostIp
is set to 127.0.0.1
.
+1 for this.
I'm working on an application that can be accessed from multiple subdomains, and requires a cookie to be shared across them. Shared cookies don't work on localhost
so I configure special local domains using mkcert such as one.myapp.local
and two.myapp.local
, and update my hosts
file accordingly. I set launchUrl
in launchSettings.json
to launch the dns name in my browser (one.myapp.local
). This all works fine when I launch the project standalone from VSCode or Rider.
However I'm transitioning to Aspire, and I can't figure out a way to get the Aspire dashboard to show my actual launch url (one.myapp.local
instead of localhost
).
I don't really understand the security argument here. I just need the Aspire dashboard to show the launchUrl
I put in launchSettings.json
In this post I provide a workaround for displaying URLs with custom domains/hosts in the dashboard. You still have to manage your hosts file and create your certificates if needed.
https://anthonysimmon.com/dotnet-aspire-non-localhost-endpoints/
Before we can use aspire we need to be able to use hostnames. We have an identity server running locally, it uses b2c on azure to do the actual authentication. b2c expects the hostname of the identityserver as valid redirect, adding localhost:4042 to b2c is not an option for us. also we have vite running with hot module replacement that also needs a hostname so it works with both iis or aspire. And it works much easier in the browser when you have a recognisable hostname versus random portnumber.
I was able to achieve a working PoC of what I needed with some inspiration from @asimmon and @mitchdenny mentions of the ResourceNotificationService
and IDistributedApplicationLifecycleHook
. IDistributedApplicationLifecycleHook
The code is just a basic happy path implementation. Haven't given much thought around what to do if there was a use case if one resource has a reference to another resource where these endpoints are added and how you would discover them cleanly. But it does work.
var permissionApi = builder.AddProject<Projects.Permissions_Api>("permissionsapi")
var productApi = builder.AddProject<Projects.Product_Api>("productapi")
.WithReference(userApi);
var userApi = builder.AddProject<Projects.User_Api>("userapi")
.WithReference(permissionsApi)
.WithReference(productApi);
var identityApi = builder.AddProject<Projects.Identity_Api>("identityapi")
.WithReference(permissionsApi)
.WithReference(userApi);
var authUI = builder.AddProject<Projects.AuthUI_Api>("authui")
.WithHostname(["auth.subsidiary-a.com", "auth.subsidiary-b.com", "auth.subsidiary-c.com"], excludeLocalhost: true)
.WithReference(identityApi);
var dashboardApi = builder.AddProject<Projects.Dashboard_Api>("dashboardapi")
.WithHostname(["dashboard.subsidiary-a.com", "dashboard.subsidiary-b.com", "dashboard.subsidiary-c.com"])
.WithReference(userApi)
.WithReference(productApi);
var dashboardClient = builder.AddNpmApp("dashboardclient", "../../Dashboard/src/AngularClient", "start")
.WithHostname(["dashboard.subsidiary-a.com", "dashboard.subsidiary-b.com", "dashboard.subsidiary-c.com"], excludeLocalhost: true)
.WithReference(dashboardApi)
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();
var adminApi = builder.AddProject<Projects.Admin_Api>("adminapi")
.WithHostname("admin.company-parent.com")
.WithReference(userApi)
.WithReference(productApi)
.WithReference(permissionsApi);
var adminClient = builder.AddNpmApp("adminclient", "../../Admin/src/AngularClient", "start")
.WithHostname("admin.company-parent.com", excludeLocalhost: true)
.WithReference(adminApi)
.WithHttpEndpoint(env: "PORT")
.WithExternalHttpEndpoints();
builder.Build().Run();
public static class ResourceHostnameExtensions
{
public static IResourceBuilder<T> WithHostname<T>(this IResourceBuilder<T> builder, string hostname, bool excludeLocalhost = false)
where T : IResourceWithEndpoints
{
builder.ApplicationBuilder.Services.TryAddLifecycleHook<ResourceHostnameLifecycleHook>();
return builder.WithAnnotation(new HostnameAnnotation([hostname], excludeLocalhost));
}
public static IResourceBuilder<T> WithHostname<T>(this IResourceBuilder<T> builder, List<string> hostnames, bool excludeLocalhost = false)
where T : IResourceWithEndpoints
{
builder.ApplicationBuilder.Services.TryAddLifecycleHook<ResourceHostnameLifecycleHook>();
return builder.WithAnnotation(new HostnameAnnotation(hostnames, excludeLocalhost));
}
private class HostnameAnnotation : IResourceAnnotation
{
public HostnameAnnotation([EndpointName] List<string> hostnames, bool excludeLocalhost)
{
ArgumentNullException.ThrowIfNull(hostnames);
if (!excludeLocalhost)
{
hostnames.Add("localhost");
}
this.Hostnames = hostnames;
}
public List<string> Hostnames { get; }
}
private class ResourceHostnameLifecycleHook(ResourceNotificationService notificationService) : IDistributedApplicationLifecycleHook
{
public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
_ = EnsureUrlsAsync();
return Task.CompletedTask;
}
private async Task EnsureUrlsAsync()
{
await foreach (var evt in notificationService.WatchAsync())
{
if (evt.Snapshot.State != KnownResourceStates.Running || evt.Resource is not IResourceWithEndpoints resource)
{
// By default, .NET Aspire only displays endpoints for running resources.
continue;
}
var hostnames = evt.Resource.Annotations.OfType<HostnameAnnotation>().SelectMany(a => a.Hostnames).Distinct();
var endpoints = evt.Resource.Annotations.OfType<EndpointAnnotation>();
var urlsToAdd = ImmutableArray.CreateBuilder<UrlSnapshot>();
foreach (var hostname in hostnames)
{
foreach (var endpoint in endpoints)
{
if (endpoint.AllocatedEndpoint is null)
{
continue;
}
var url = $"{endpoint.UriScheme}://{hostname}:{endpoint.AllocatedEndpoint.Port}";
if (!Uri.TryCreate(url, UriKind.Absolute, out _))
{
throw new ArgumentException($"'{url}' is not an absolute URL.", nameof(url));
}
var urlAlreadyAdded = evt.Snapshot.Urls.Any(x => string.Equals(x.Name, hostname, StringComparison.OrdinalIgnoreCase));
if (!urlAlreadyAdded)
{
urlsToAdd.Add(new UrlSnapshot(hostname, url, IsInternal: false));
}
}
}
if (urlsToAdd.Count > 0)
{
await notificationService.PublishUpdateAsync(evt.Resource, snapshot => snapshot with
{
Urls = urlsToAdd.ToImmutableArray()
});
}
}
}
}
}
@davidfowl @mitchdenny This issue I'm trying to solve (or feature enhancement) is related to not being able to provide a specific hostname/address alias to resource.
I have a couple use cases:
So it would be awesome to be able supply additional address bindings specifically for local hosting that ALSO display on the dashboard. (This was something I recall doing with Tye)
An existing
tye.yaml
A "kinda" workaround
I've been adapting an existing distributed application into the Aspire model. My goal has been to do this incrementally without being invasive or making modifications to the existing projects to prevent any refactor disruptions or coordination. I have been successful enough so far, so kudos on baking in that flexibility from the start!
The workaround I have found like below so far appears to allow a workable path but not ideal of course from the dashboard or verbosity.