diff --git a/.gitignore b/.gitignore index b5fc4713..d1473933 100644 --- a/.gitignore +++ b/.gitignore @@ -443,3 +443,6 @@ FodyWeavers.xsd # tmp directory tmp/ + +# Transient Aspire publish manifest generated by the manifest verification tests +**/aspire-manifest.g.json diff --git a/samples/Metrics/MetricsApp.AppHost/aspire-manifest.json b/samples/Metrics/MetricsApp.AppHost/aspire-manifest.json new file mode 100644 index 00000000..1ac340fe --- /dev/null +++ b/samples/Metrics/MetricsApp.AppHost/aspire-manifest.json @@ -0,0 +1,107 @@ +{ + "resources": { + "prometheus": { + "type": "container.v0", + "image": "prom/prometheus:v3.2.1", + "args": [ + "--web.enable-otlp-receiver", + "--config.file=/etc/prometheus/prometheus.yml" + ], + "bindMounts": [ + { + "source": "../prometheus", + "target": "/etc/prometheus", + "readOnly": true + } + ], + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 9090 + } + } + }, + "grafana": { + "type": "container.v0", + "image": "grafana/grafana:latest", + "bindMounts": [ + { + "source": "../grafana/config", + "target": "/etc/grafana", + "readOnly": true + }, + { + "source": "../grafana/dashboards", + "target": "/var/lib/grafana/dashboards", + "readOnly": true + } + ], + "env": { + "PROMETHEUS_ENDPOINT": "{prometheus.bindings.http.url}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 3000 + } + } + }, + "otelcollector": { + "type": "container.v0", + "image": "ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.123.0", + "bindMounts": [ + { + "source": "../otelcollector/config.yaml", + "target": "/etc/otelcol-contrib/config.yaml", + "readOnly": false + } + ], + "env": { + "ASPIRE_ENDPOINT": "http://localhost:18889", + "ASPIRE_API_KEY": "", + "ASPIRE_INSECURE": "true", + "PROMETHEUS_ENDPOINT": "{prometheus.bindings.http.url}/api/v1/otlp" + }, + "bindings": { + "grpc": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 4317 + }, + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 4318 + } + } + }, + "app": { + "type": "project.v0", + "path": "../MetricsApp/MetricsApp.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{app.bindings.http.targetPort}", + "GRAFANA_URL": "{grafana.bindings.http.url}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + } + } +} \ No newline at end of file diff --git a/samples/aspire-shop/AspireShop.AppHost/aspire-manifest.json b/samples/aspire-shop/AspireShop.AppHost/aspire-manifest.json new file mode 100644 index 00000000..fea14c66 --- /dev/null +++ b/samples/aspire-shop/AspireShop.AppHost/aspire-manifest.json @@ -0,0 +1,215 @@ +{ + "resources": { + "postgres": { + "type": "container.v0", + "connectionString": "Host={postgres.bindings.tcp.host};Port={postgres.bindings.tcp.port};Username=postgres;Password={postgres-password.value}", + "image": "docker.io/library/postgres:18.3", + "env": { + "POSTGRES_HOST_AUTH_METHOD": "scram-sha-256", + "POSTGRES_INITDB_ARGS": "--auth-host=scram-sha-256 --auth-local=scram-sha-256", + "POSTGRES_USER": "postgres", + "POSTGRES_PASSWORD": "{postgres-password.value}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 5432 + } + } + }, + "catalogdb": { + "type": "value.v0", + "connectionString": "{postgres.connectionString};Database=catalogdb" + }, + "basketcache": { + "type": "container.v0", + "connectionString": "{basketcache.bindings.tcp.host}:{basketcache.bindings.tcp.port},password={basketcache-password.value}{cond-basketcache-bindings-tcp-tlsenabled-8c2280ff.connectionString}", + "image": "docker.io/library/redis:8.6", + "entrypoint": "/bin/sh", + "args": [ + "-c", + "redis-server --requirepass $REDIS_PASSWORD --save 60 1" + ], + "volumes": [ + { + "name": "aspireshop.apphost--basketcache-data", + "target": "/data", + "readOnly": false + } + ], + "env": { + "REDIS_PASSWORD": "{basketcache-password.value}" + }, + "bindings": { + "tcp": { + "scheme": "redis", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 6379 + } + } + }, + "catalogdbmanager": { + "type": "project.v0", + "path": "../AspireShop.CatalogDbManager/AspireShop.CatalogDbManager.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{catalogdbmanager.bindings.http.targetPort}", + "ConnectionStrings__catalogdb": "{catalogdb.connectionString}", + "CATALOGDB_HOST": "{postgres.bindings.tcp.host}", + "CATALOGDB_PORT": "{postgres.bindings.tcp.port}", + "CATALOGDB_USERNAME": "postgres", + "CATALOGDB_PASSWORD": "{postgres-password.value}", + "CATALOGDB_URI": "postgresql://postgres:{postgres-password-uri-encoded.value}@{postgres.bindings.tcp.host}:{postgres.bindings.tcp.port}/catalogdb", + "CATALOGDB_JDBCCONNECTIONSTRING": "jdbc:postgresql://{postgres.bindings.tcp.host}:{postgres.bindings.tcp.port}/catalogdb", + "CATALOGDB_DATABASENAME": "catalogdb" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + }, + "catalogservice": { + "type": "project.v0", + "path": "../AspireShop.CatalogService/AspireShop.CatalogService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{catalogservice.bindings.http.targetPort}", + "ConnectionStrings__catalogdb": "{catalogdb.connectionString}", + "CATALOGDB_HOST": "{postgres.bindings.tcp.host}", + "CATALOGDB_PORT": "{postgres.bindings.tcp.port}", + "CATALOGDB_USERNAME": "postgres", + "CATALOGDB_PASSWORD": "{postgres-password.value}", + "CATALOGDB_URI": "postgresql://postgres:{postgres-password-uri-encoded.value}@{postgres.bindings.tcp.host}:{postgres.bindings.tcp.port}/catalogdb", + "CATALOGDB_JDBCCONNECTIONSTRING": "jdbc:postgresql://{postgres.bindings.tcp.host}:{postgres.bindings.tcp.port}/catalogdb", + "CATALOGDB_DATABASENAME": "catalogdb" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + }, + "basketservice": { + "type": "project.v0", + "path": "../AspireShop.BasketService/AspireShop.BasketService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{basketservice.bindings.http.targetPort}", + "ConnectionStrings__basketcache": "{basketcache.connectionString}", + "BASKETCACHE_HOST": "{basketcache.bindings.tcp.host}", + "BASKETCACHE_PORT": "{basketcache.bindings.tcp.port}", + "BASKETCACHE_PASSWORD": "{basketcache-password.value}", + "BASKETCACHE_URI": "{basketcache.bindings.tcp.scheme}://:{basketcache-password-uri-encoded.value}@{basketcache.bindings.tcp.host}:{basketcache.bindings.tcp.port}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http2" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http2" + } + } + }, + "frontend": { + "type": "project.v0", + "path": "../AspireShop.Frontend/AspireShop.Frontend.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{frontend.bindings.http.targetPort}", + "BASKETSERVICE_HTTP": "{basketservice.bindings.http.url}", + "services__basketservice__http__0": "{basketservice.bindings.http.url}", + "BASKETSERVICE_HTTPS": "{basketservice.bindings.https.url}", + "services__basketservice__https__0": "{basketservice.bindings.https.url}", + "CATALOGSERVICE_HTTP": "{catalogservice.bindings.http.url}", + "services__catalogservice__http__0": "{catalogservice.bindings.http.url}", + "CATALOGSERVICE_HTTPS": "{catalogservice.bindings.https.url}", + "services__catalogservice__https__0": "{catalogservice.bindings.https.url}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "external": true + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http", + "external": true + } + } + }, + "postgres-password": { + "type": "parameter.v0", + "value": "{postgres-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22 + } + } + } + } + }, + "postgres-password-uri-encoded": { + "type": "annotated.string", + "value": "{postgres-password.value}", + "filter": "uri" + }, + "basketcache-password": { + "type": "parameter.v0", + "value": "{basketcache-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "special": false + } + } + } + } + }, + "basketcache-password-uri-encoded": { + "type": "annotated.string", + "value": "{basketcache-password.value}", + "filter": "uri" + }, + "cond-basketcache-bindings-tcp-tlsenabled-8c2280ff": { + "type": "value.v0", + "connectionString": "" + } + } +} \ No newline at end of file diff --git a/samples/aspire-with-azure-functions/ImageGallery.AppHost/aspire-manifest.json b/samples/aspire-with-azure-functions/ImageGallery.AppHost/aspire-manifest.json new file mode 100644 index 00000000..3e4f0994 --- /dev/null +++ b/samples/aspire-with-azure-functions/ImageGallery.AppHost/aspire-manifest.json @@ -0,0 +1,150 @@ +{ + "resources": { + "env-acr": { + "type": "azure.bicep.v0", + "path": "env-acr.module.bicep" + }, + "env": { + "type": "azure.bicep.v0", + "path": "env.module.bicep", + "params": { + "env_acr_outputs_name": "{env-acr.outputs.name}", + "userPrincipalId": "" + } + }, + "storage": { + "type": "azure.bicep.v0", + "path": "storage.module.bicep" + }, + "blobs": { + "type": "value.v0", + "connectionString": "{storage.outputs.blobEndpoint}" + }, + "queues": { + "type": "value.v0", + "connectionString": "{storage.outputs.queueEndpoint}" + }, + "functions": { + "type": "project.v1", + "path": "../ImageGallery.Functions/ImageGallery.Functions.csproj", + "deployment": { + "type": "azure.bicep.v0", + "path": "functions-containerapp.module.bicep", + "params": { + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "functions_containerimage": "{functions.containerImage}", + "functions_identity_outputs_id": "{functions-identity.outputs.id}", + "storage_outputs_blobendpoint": "{storage.outputs.blobEndpoint}", + "storage_outputs_queueendpoint": "{storage.outputs.queueEndpoint}", + "storage_outputs_tableendpoint": "{storage.outputs.tableEndpoint}", + "storage_outputs_datalakeendpoint": "{storage.outputs.dataLakeEndpoint}", + "functions_identity_outputs_clientid": "{functions-identity.outputs.clientId}" + } + }, + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", + "AzureFunctionsJobHost__telemetryMode": "OpenTelemetry", + "ASPNETCORE_URLS": "http://\u002B:{functions.bindings.http.targetPort}", + "AzureWebJobsStorage__blobServiceUri": "{storage.outputs.blobEndpoint}", + "AzureWebJobsStorage__queueServiceUri": "{storage.outputs.queueEndpoint}", + "AzureWebJobsStorage__tableServiceUri": "{storage.outputs.tableEndpoint}", + "AzureWebJobsStorage__dataLakeServiceUri": "{storage.outputs.dataLakeEndpoint}", + "Aspire__Azure__Storage__Blobs__AzureWebJobsStorage__ServiceUri": "{storage.outputs.blobEndpoint}", + "Aspire__Azure__Storage__Queues__AzureWebJobsStorage__ServiceUri": "{storage.outputs.queueEndpoint}", + "Aspire__Azure__Data__Tables__AzureWebJobsStorage__ServiceUri": "{storage.outputs.tableEndpoint}", + "Aspire__Azure__Storage__Files__DataLake__AzureWebJobsStorage__ServiceUri": "{storage.outputs.dataLakeEndpoint}", + "queues__queueServiceUri": "{storage.outputs.queueEndpoint}", + "Aspire__Azure__Storage__Queues__queues__ServiceUri": "{storage.outputs.queueEndpoint}", + "blobs__blobServiceUri": "{storage.outputs.blobEndpoint}", + "blobs__queueServiceUri": "{storage.outputs.queueEndpoint}", + "Aspire__Azure__Storage__Blobs__blobs__ServiceUri": "{storage.outputs.blobEndpoint}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 8080 + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http", + "targetPort": 8080 + } + } + }, + "frontend": { + "type": "project.v1", + "path": "../ImageGallery.FrontEnd/ImageGallery.FrontEnd.csproj", + "deployment": { + "type": "azure.bicep.v0", + "path": "frontend-containerapp.module.bicep", + "params": { + "env_outputs_azure_container_apps_environment_default_domain": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN}", + "env_outputs_azure_container_apps_environment_id": "{env.outputs.AZURE_CONTAINER_APPS_ENVIRONMENT_ID}", + "env_outputs_azure_container_registry_endpoint": "{env.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT}", + "env_outputs_azure_container_registry_managed_identity_id": "{env.outputs.AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID}", + "frontend_containerimage": "{frontend.containerImage}", + "frontend_identity_outputs_id": "{frontend-identity.outputs.id}", + "frontend_containerport": "{frontend.containerPort}", + "storage_outputs_queueendpoint": "{storage.outputs.queueEndpoint}", + "storage_outputs_blobendpoint": "{storage.outputs.blobEndpoint}", + "frontend_identity_outputs_clientid": "{frontend-identity.outputs.clientId}" + } + }, + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{frontend.bindings.http.targetPort}", + "ConnectionStrings__queues": "{queues.connectionString}", + "QUEUES_URI": "{storage.outputs.queueEndpoint}", + "ConnectionStrings__blobs": "{blobs.connectionString}", + "BLOBS_URI": "{storage.outputs.blobEndpoint}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "external": true + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http", + "external": true + } + } + }, + "functions-identity": { + "type": "azure.bicep.v0", + "path": "functions-identity.module.bicep" + }, + "functions-roles-storage": { + "type": "azure.bicep.v0", + "path": "functions-roles-storage.module.bicep", + "params": { + "storage_outputs_name": "{storage.outputs.name}", + "principalId": "{functions-identity.outputs.principalId}" + } + }, + "frontend-identity": { + "type": "azure.bicep.v0", + "path": "frontend-identity.module.bicep" + }, + "frontend-roles-storage": { + "type": "azure.bicep.v0", + "path": "frontend-roles-storage.module.bicep", + "params": { + "storage_outputs_name": "{storage.outputs.name}", + "principalId": "{frontend-identity.outputs.principalId}" + } + } + } +} \ No newline at end of file diff --git a/samples/aspire-with-javascript/AspireJavaScript.AppHost/aspire-manifest.json b/samples/aspire-with-javascript/AspireJavaScript.AppHost/aspire-manifest.json index 4b9f100e..8a70514e 100644 --- a/samples/aspire-with-javascript/AspireJavaScript.AppHost/aspire-manifest.json +++ b/samples/aspire-with-javascript/AspireJavaScript.AppHost/aspire-manifest.json @@ -1,12 +1,17 @@ { - "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { "weatherapi": { "type": "project.v0", "path": "../AspireJavaScript.MinimalApi/AspireJavaScript.MinimalApi.csproj", + "containerFiles": { + "reactvite": { + "destination": "./wwwroot", + "sources": [ + "/app/dist" + ] + } + }, "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", "HTTP_PORTS": "{weatherapi.bindings.http.targetPort}" @@ -26,66 +31,4 @@ } } }, - "angular": { - "type": "dockerfile.v0", - "path": "../AspireJavaScript.Angular/Dockerfile", - "context": "../AspireJavaScript.Angular", - "env": { - "NODE_ENV": "development", - "services__weatherapi__http__0": "{weatherapi.bindings.http.url}", - "services__weatherapi__https__0": "{weatherapi.bindings.https.url}", - "PORT": "{angular.bindings.http.targetPort}" - }, - "bindings": { - "http": { - "scheme": "http", - "protocol": "tcp", - "transport": "http", - "targetPort": 8000, - "external": true - } - } - }, - "react": { - "type": "dockerfile.v0", - "path": "../AspireJavaScript.React/Dockerfile", - "context": "../AspireJavaScript.React", - "env": { - "NODE_ENV": "development", - "services__weatherapi__http__0": "{weatherapi.bindings.http.url}", - "services__weatherapi__https__0": "{weatherapi.bindings.https.url}", - "BROWSER": "none", - "PORT": "{react.bindings.http.targetPort}" - }, - "bindings": { - "http": { - "scheme": "http", - "protocol": "tcp", - "transport": "http", - "targetPort": 8001, - "external": true - } - } - }, - "vue": { - "type": "dockerfile.v0", - "path": "../AspireJavaScript.Vue/Dockerfile", - "context": "../AspireJavaScript.Vue", - "env": { - "NODE_ENV": "development", - "services__weatherapi__http__0": "{weatherapi.bindings.http.url}", - "services__weatherapi__https__0": "{weatherapi.bindings.https.url}", - "PORT": "{vue.bindings.http.targetPort}" - }, - "bindings": { - "http": { - "scheme": "http", - "protocol": "tcp", - "transport": "http", - "targetPort": 8002, - "external": true - } - } - } - } -} \ No newline at end of file + "angular": { \ No newline at end of file diff --git a/samples/aspire-with-node/AspireWithNode.AppHost/aspire-manifest.json b/samples/aspire-with-node/AspireWithNode.AppHost/aspire-manifest.json index c5ea66d0..c4f6d5ff 100644 --- a/samples/aspire-with-node/AspireWithNode.AppHost/aspire-manifest.json +++ b/samples/aspire-with-node/AspireWithNode.AppHost/aspire-manifest.json @@ -2,11 +2,19 @@ "resources": { "cache": { "type": "container.v0", - "connectionString": "{cache.bindings.tcp.host}:{cache.bindings.tcp.port}", - "image": "docker.io/library/redis:7.2.4", + "connectionString": "{cache.bindings.tcp.host}:{cache.bindings.tcp.port},password={cache-password.value}{cond-cache-bindings-tcp-tlsenabled-3eddb73a.connectionString}", + "image": "docker.io/library/redis:8.6", + "entrypoint": "/bin/sh", + "args": [ + "-c", + "redis-server --requirepass $REDIS_PASSWORD" + ], + "env": { + "REDIS_PASSWORD": "{cache-password.value}" + }, "bindings": { "tcp": { - "scheme": "tcp", + "scheme": "redis", "protocol": "tcp", "transport": "tcp", "targetPort": 6379 @@ -17,10 +25,9 @@ "type": "project.v0", "path": "../AspireWithNode.AspNetCoreApi/AspireWithNode.AspNetCoreApi.csproj", "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", - "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true" + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{weatherapi.bindings.http.targetPort}" }, "bindings": { "http": { @@ -36,25 +43,58 @@ } }, "frontend": { - "type": "dockerfile.v0", - "path": "../NodeFrontend/Dockerfile", - "context": "../NodeFrontend", + "type": "container.v1", + "build": { + "context": "../NodeFrontend", + "dockerfile": "frontend.Dockerfile" + }, "env": { - "NODE_ENV": "production", + "NODE_ENV": "development", + "PORT": "{frontend.bindings.http.targetPort}", + "WEATHERAPI_HTTP": "{weatherapi.bindings.http.url}", "services__weatherapi__http__0": "{weatherapi.bindings.http.url}", + "WEATHERAPI_HTTPS": "{weatherapi.bindings.https.url}", "services__weatherapi__https__0": "{weatherapi.bindings.https.url}", "ConnectionStrings__cache": "{cache.connectionString}", - "PORT": "{frontend.bindings.http.targetPort}" + "CACHE_HOST": "{cache.bindings.tcp.host}", + "CACHE_PORT": "{cache.bindings.tcp.port}", + "CACHE_PASSWORD": "{cache-password.value}", + "CACHE_URI": "{cache.bindings.tcp.scheme}://:{cache-password-uri-encoded.value}@{cache.bindings.tcp.host}:{cache.bindings.tcp.port}" }, "bindings": { "http": { "scheme": "http", "protocol": "tcp", "transport": "http", - "targetPort": 8000, + "targetPort": 5223, "external": true } } + }, + "cache-password": { + "type": "parameter.v0", + "value": "{cache-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "special": false + } + } + } + } + }, + "cache-password-uri-encoded": { + "type": "annotated.string", + "value": "{cache-password.value}", + "filter": "uri" + }, + "cond-cache-bindings-tcp-tlsenabled-3eddb73a": { + "type": "value.v0", + "connectionString": "" } } } \ No newline at end of file diff --git a/samples/client-apps-integration/ClientAppsIntegration.AppHost/aspire-manifest.json b/samples/client-apps-integration/ClientAppsIntegration.AppHost/aspire-manifest.json new file mode 100644 index 00000000..ffb0b119 --- /dev/null +++ b/samples/client-apps-integration/ClientAppsIntegration.AppHost/aspire-manifest.json @@ -0,0 +1,25 @@ +{ + "resources": { + "apiservice": { + "type": "project.v0", + "path": "../ClientAppsIntegration.ApiService/ClientAppsIntegration.ApiService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{apiservice.bindings.http.targetPort}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + } + } +} \ No newline at end of file diff --git a/samples/custom-resources/CustomResources.AppHost/aspire-manifest.json b/samples/custom-resources/CustomResources.AppHost/aspire-manifest.json new file mode 100644 index 00000000..38ac6a7c --- /dev/null +++ b/samples/custom-resources/CustomResources.AppHost/aspire-manifest.json @@ -0,0 +1,10 @@ +{ + "resources": { + "talking-clock-tick-hand": { + "error": "This resource does not support generation in the manifest." + }, + "talking-clock-tock-hand": { + "error": "This resource does not support generation in the manifest." + } + } +} \ No newline at end of file diff --git a/samples/database-containers/DatabaseContainers.AppHost/aspire-manifest.json b/samples/database-containers/DatabaseContainers.AppHost/aspire-manifest.json new file mode 100644 index 00000000..225ed6f6 --- /dev/null +++ b/samples/database-containers/DatabaseContainers.AppHost/aspire-manifest.json @@ -0,0 +1,213 @@ +{ + "resources": { + "postgres": { + "type": "container.v0", + "connectionString": "Host={postgres.bindings.tcp.host};Port={postgres.bindings.tcp.port};Username=postgres;Password={postgres-password.value}", + "image": "docker.io/library/postgres:18.3", + "bindMounts": [ + { + "source": "../DatabaseContainers.ApiService/data/postgres", + "target": "/docker-entrypoint-initdb.d", + "readOnly": false + } + ], + "volumes": [ + { + "name": "databasecontainers.apphost--postgres-data", + "target": "/var/lib/postgresql", + "readOnly": false + } + ], + "env": { + "POSTGRES_HOST_AUTH_METHOD": "scram-sha-256", + "POSTGRES_INITDB_ARGS": "--auth-host=scram-sha-256 --auth-local=scram-sha-256", + "POSTGRES_USER": "postgres", + "POSTGRES_PASSWORD": "{postgres-password.value}", + "POSTGRES_DB": "Todos" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 5432 + } + } + }, + "Todos": { + "type": "value.v0", + "connectionString": "{postgres.connectionString};Database=Todos" + }, + "mysql": { + "type": "container.v0", + "connectionString": "Server={mysql.bindings.tcp.host};Port={mysql.bindings.tcp.port};User ID=root;Password={mysql-password.value}", + "image": "docker.io/library/mysql:9.7", + "bindMounts": [ + { + "source": "../DatabaseContainers.ApiService/data/mysql", + "target": "/docker-entrypoint-initdb.d", + "readOnly": false + } + ], + "volumes": [ + { + "name": "databasecontainers.apphost--mysql-data", + "target": "/var/lib/mysql", + "readOnly": false + } + ], + "env": { + "MYSQL_ROOT_PASSWORD": "{mysql-password.value}", + "MYSQL_DATABASE": "catalog" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 3306 + } + } + }, + "catalog": { + "type": "value.v0", + "connectionString": "{mysql.connectionString};Database=catalog" + }, + "sqlserver": { + "type": "container.v0", + "connectionString": "Server={sqlserver.bindings.tcp.host},{sqlserver.bindings.tcp.port};User ID=sa;Password={sqlserver-password.value};TrustServerCertificate=true", + "image": "mcr.microsoft.com/mssql/server:2022-latest", + "volumes": [ + { + "name": "databasecontainers.apphost--sqlserver-data", + "target": "/var/opt/mssql", + "readOnly": false + } + ], + "env": { + "ACCEPT_EULA": "Y", + "MSSQL_SA_PASSWORD": "{sqlserver-password.value}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 1433 + } + } + }, + "AddressBook": { + "type": "value.v0", + "connectionString": "{sqlserver.connectionString};Initial Catalog=AddressBook" + }, + "apiservice": { + "type": "project.v0", + "path": "../DatabaseContainers.ApiService/DatabaseContainers.ApiService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{apiservice.bindings.http.targetPort}", + "ConnectionStrings__Todos": "{Todos.connectionString}", + "TODOS_HOST": "{postgres.bindings.tcp.host}", + "TODOS_PORT": "{postgres.bindings.tcp.port}", + "TODOS_USERNAME": "postgres", + "TODOS_PASSWORD": "{postgres-password.value}", + "TODOS_URI": "postgresql://postgres:{postgres-password-uri-encoded.value}@{postgres.bindings.tcp.host}:{postgres.bindings.tcp.port}/Todos", + "TODOS_JDBCCONNECTIONSTRING": "jdbc:postgresql://{postgres.bindings.tcp.host}:{postgres.bindings.tcp.port}/Todos", + "TODOS_DATABASENAME": "Todos", + "ConnectionStrings__catalog": "{catalog.connectionString}", + "CATALOG_HOST": "{mysql.bindings.tcp.host}", + "CATALOG_PORT": "{mysql.bindings.tcp.port}", + "CATALOG_USERNAME": "root", + "CATALOG_PASSWORD": "{mysql-password.value}", + "CATALOG_URI": "mysql://root:{mysql-password-uri-encoded.value}@{mysql.bindings.tcp.host}:{mysql.bindings.tcp.port}/catalog", + "CATALOG_JDBCCONNECTIONSTRING": "jdbc:mysql://{mysql.bindings.tcp.host}:{mysql.bindings.tcp.port}/catalog", + "CATALOG_DATABASENAME": "catalog", + "ConnectionStrings__AddressBook": "{AddressBook.connectionString}", + "ADDRESSBOOK_HOST": "{sqlserver.bindings.tcp.host}", + "ADDRESSBOOK_PORT": "{sqlserver.bindings.tcp.port}", + "ADDRESSBOOK_USERNAME": "sa", + "ADDRESSBOOK_PASSWORD": "{sqlserver-password.value}", + "ADDRESSBOOK_URI": "mssql://sa:{sqlserver-password.value}@{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port}/AddressBook", + "ADDRESSBOOK_JDBCCONNECTIONSTRING": "jdbc:sqlserver://{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port};databaseName=AddressBook;trustServerCertificate=true", + "ADDRESSBOOK_DATABASENAME": "AddressBook" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + }, + "postgres-password": { + "type": "parameter.v0", + "value": "{postgres-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22 + } + } + } + } + }, + "postgres-password-uri-encoded": { + "type": "annotated.string", + "value": "{postgres-password.value}", + "filter": "uri" + }, + "mysql-password": { + "type": "parameter.v0", + "value": "{mysql-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22 + } + } + } + } + }, + "mysql-password-uri-encoded": { + "type": "annotated.string", + "value": "{mysql-password.value}", + "filter": "uri" + }, + "sqlserver-password": { + "type": "parameter.v0", + "value": "{sqlserver-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "minLower": 1, + "minUpper": 1, + "minNumeric": 1 + } + } + } + } + }, + "sqlserver-password-uri-encoded": { + "type": "annotated.string", + "value": "{sqlserver-password.value}", + "filter": "uri" + } + } +} \ No newline at end of file diff --git a/samples/database-migrations/DatabaseMigrations.AppHost/aspire-manifest.json b/samples/database-migrations/DatabaseMigrations.AppHost/aspire-manifest.json new file mode 100644 index 00000000..9848bc25 --- /dev/null +++ b/samples/database-migrations/DatabaseMigrations.AppHost/aspire-manifest.json @@ -0,0 +1,99 @@ +{ + "resources": { + "sqlserver": { + "type": "container.v0", + "connectionString": "Server={sqlserver.bindings.tcp.host},{sqlserver.bindings.tcp.port};User ID=sa;Password={sqlserver-password.value};TrustServerCertificate=true", + "image": "mcr.microsoft.com/mssql/server:2022-latest", + "volumes": [ + { + "name": "databasemigrations.apphost--sqlserver-data", + "target": "/var/opt/mssql", + "readOnly": false + } + ], + "env": { + "ACCEPT_EULA": "Y", + "MSSQL_SA_PASSWORD": "{sqlserver-password.value}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 1433 + } + } + }, + "db1": { + "type": "value.v0", + "connectionString": "{sqlserver.connectionString};Initial Catalog=db1" + }, + "migration": { + "type": "project.v0", + "path": "../DatabaseMigrations.MigrationService/DatabaseMigrations.MigrationService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ConnectionStrings__db1": "{db1.connectionString}", + "DB1_HOST": "{sqlserver.bindings.tcp.host}", + "DB1_PORT": "{sqlserver.bindings.tcp.port}", + "DB1_USERNAME": "sa", + "DB1_PASSWORD": "{sqlserver-password.value}", + "DB1_URI": "mssql://sa:{sqlserver-password.value}@{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port}/db1", + "DB1_JDBCCONNECTIONSTRING": "jdbc:sqlserver://{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port};databaseName=db1;trustServerCertificate=true", + "DB1_DATABASENAME": "db1" + } + }, + "api": { + "type": "project.v0", + "path": "../DatabaseMigrations.ApiService/DatabaseMigrations.ApiService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{api.bindings.http.targetPort}", + "ConnectionStrings__db1": "{db1.connectionString}", + "DB1_HOST": "{sqlserver.bindings.tcp.host}", + "DB1_PORT": "{sqlserver.bindings.tcp.port}", + "DB1_USERNAME": "sa", + "DB1_PASSWORD": "{sqlserver-password.value}", + "DB1_URI": "mssql://sa:{sqlserver-password.value}@{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port}/db1", + "DB1_JDBCCONNECTIONSTRING": "jdbc:sqlserver://{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port};databaseName=db1;trustServerCertificate=true", + "DB1_DATABASENAME": "db1" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + }, + "sqlserver-password": { + "type": "parameter.v0", + "value": "{sqlserver-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "minLower": 1, + "minUpper": 1, + "minNumeric": 1 + } + } + } + } + }, + "sqlserver-password-uri-encoded": { + "type": "annotated.string", + "value": "{sqlserver-password.value}", + "filter": "uri" + } + } +} \ No newline at end of file diff --git a/samples/health-checks-ui/HealthChecksUI.AppHost/aspire-manifest.json b/samples/health-checks-ui/HealthChecksUI.AppHost/aspire-manifest.json index be646541..315ef694 100644 --- a/samples/health-checks-ui/HealthChecksUI.AppHost/aspire-manifest.json +++ b/samples/health-checks-ui/HealthChecksUI.AppHost/aspire-manifest.json @@ -1,13 +1,23 @@ { - "$schema": "https://json.schemastore.org/aspire-8.0.json", "resources": { + "compose": { + "error": "This resource does not support generation in the manifest." + }, "cache": { - "type": "container.v0", - "connectionString": "{cache.bindings.tcp.host}:{cache.bindings.tcp.port}", - "image": "docker.io/library/redis:7.4", + "type": "container.v1", + "connectionString": "{cache.bindings.tcp.host}:{cache.bindings.tcp.port},password={cache-password.value}{cond-cache-bindings-tcp-tlsenabled-3eddb73a.connectionString}", + "image": "docker.io/library/redis:8.6", + "entrypoint": "/bin/sh", + "args": [ + "-c", + "redis-server --requirepass $REDIS_PASSWORD" + ], + "env": { + "REDIS_PASSWORD": "{cache-password.value}" + }, "bindings": { "tcp": { - "scheme": "tcp", + "scheme": "redis", "protocol": "tcp", "transport": "tcp", "targetPort": 6379 @@ -15,15 +25,16 @@ } }, "apiservice": { - "type": "project.v0", + "type": "project.v1", "path": "../HealthChecksUI.ApiService/HealthChecksUI.ApiService.csproj", "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", "HTTP_PORTS": "{apiservice.bindings.http.targetPort};{apiservice.bindings.healthchecks.targetPort}", - "HEALTHCHECKSUI_URLS": "{apiservice.bindings.healthchecks.url}/healthz" + "HEALTHCHECKSUI_URLS": "{apiservice.bindings.healthchecks.url}/healthz", + "OTEL_EXPORTER_OTLP_ENDPOINT": "{compose-dashboard.bindings.otlp-grpc.url}", + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + "OTEL_SERVICE_NAME": "apiservice" }, "bindings": { "http": { @@ -40,24 +51,32 @@ "scheme": "http", "protocol": "tcp", "transport": "http", - "targetPort": 8000 + "targetPort": 8081 } } }, "webfrontend": { - "type": "project.v0", + "type": "project.v1", "path": "../HealthChecksUI.Web/HealthChecksUI.Web.csproj", "env": { - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true", - "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true", "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", "HTTP_PORTS": "{webfrontend.bindings.http.targetPort};{webfrontend.bindings.healthchecks.targetPort}", "ConnectionStrings__cache": "{cache.connectionString}", + "CACHE_HOST": "{cache.bindings.tcp.host}", + "CACHE_PORT": "{cache.bindings.tcp.port}", + "CACHE_PASSWORD": "{cache-password.value}", + "CACHE_URI": "{cache.bindings.tcp.scheme}://:{cache-password-uri-encoded.value}@{cache.bindings.tcp.host}:{cache.bindings.tcp.port}", + "APISERVICE_HTTP": "{apiservice.bindings.http.url}", "services__apiservice__http__0": "{apiservice.bindings.http.url}", + "APISERVICE_HTTPS": "{apiservice.bindings.https.url}", "services__apiservice__https__0": "{apiservice.bindings.https.url}", + "APISERVICE_HEALTHCHECKS": "{apiservice.bindings.healthchecks.url}", "services__apiservice__healthchecks__0": "{apiservice.bindings.healthchecks.url}", - "HEALTHCHECKSUI_URLS": "{webfrontend.bindings.healthchecks.url}/healthz" + "HEALTHCHECKSUI_URLS": "{webfrontend.bindings.healthchecks.url}/healthz", + "OTEL_EXPORTER_OTLP_ENDPOINT": "{compose-dashboard.bindings.otlp-grpc.url}", + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + "OTEL_SERVICE_NAME": "webfrontend" }, "bindings": { "http": { @@ -76,12 +95,12 @@ "scheme": "http", "protocol": "tcp", "transport": "http", - "targetPort": 8001 + "targetPort": 8081 } } }, "healthchecksui": { - "type": "container.v0", + "type": "container.v1", "image": "docker.io/xabarilcoding/healthchecksui:5.0.0", "env": { "ui_path": "/", @@ -99,6 +118,56 @@ "external": true } } + }, + "cache-password": { + "type": "parameter.v0", + "value": "{cache-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "special": false + } + } + } + } + }, + "cache-password-uri-encoded": { + "type": "annotated.string", + "value": "{cache-password.value}", + "filter": "uri" + }, + "compose-dashboard": { + "type": "container.v1", + "image": "mcr.microsoft.com/dotnet/nightly/aspire-dashboard:latest", + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 18888, + "external": true + }, + "otlp-grpc": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 18889 + }, + "otlp-http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "targetPort": 18890 + } + } + }, + "cond-cache-bindings-tcp-tlsenabled-3eddb73a": { + "type": "value.v0", + "connectionString": "" } } } \ No newline at end of file diff --git a/samples/orleans-voting/OrleansVoting.AppHost/aspire-manifest.json b/samples/orleans-voting/OrleansVoting.AppHost/aspire-manifest.json new file mode 100644 index 00000000..787838f4 --- /dev/null +++ b/samples/orleans-voting/OrleansVoting.AppHost/aspire-manifest.json @@ -0,0 +1,131 @@ +{ + "resources": { + "voting-redis": { + "type": "container.v0", + "connectionString": "{voting-redis.bindings.tcp.host}:{voting-redis.bindings.tcp.port},password={voting-redis-password.value}{cond-voting-redis-bindings-tcp-tlsenabled-2812f32c.connectionString}", + "image": "docker.io/library/redis:8.6", + "entrypoint": "/bin/sh", + "args": [ + "-c", + "redis-server --requirepass $REDIS_PASSWORD" + ], + "env": { + "REDIS_PASSWORD": "{voting-redis-password.value}" + }, + "bindings": { + "tcp": { + "scheme": "redis", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 6379 + } + } + }, + "voting-fe": { + "type": "project.v0", + "path": "../OrleansVoting.Service/OrleansVoting.Service.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{voting-fe.bindings.http.targetPort}", + "Orleans__Clustering__ProviderType": "Redis", + "Orleans__Clustering__ServiceKey": "voting-redis", + "ConnectionStrings__voting-redis": "{voting-redis.connectionString}", + "VOTING_REDIS_HOST": "{voting-redis.bindings.tcp.host}", + "VOTING_REDIS_PORT": "{voting-redis.bindings.tcp.port}", + "VOTING_REDIS_PASSWORD": "{voting-redis-password.value}", + "VOTING_REDIS_URI": "{voting-redis.bindings.tcp.scheme}://:{voting-redis-password-uri-encoded.value}@{voting-redis.bindings.tcp.host}:{voting-redis.bindings.tcp.port}", + "Orleans__ClusterId": "{voting-cluster-cluster-id.value}", + "Orleans__ServiceId": "{voting-cluster-service-id.value}", + "Orleans__EnableDistributedTracing": "true", + "Orleans__GrainStorage__votes__ProviderType": "Redis", + "Orleans__GrainStorage__votes__ServiceKey": "voting-redis", + "Orleans__Endpoints__SiloPort": "{voting-fe.bindings.orleans-silo.targetPort}", + "Orleans__Endpoints__GatewayPort": "{voting-fe.bindings.orleans-gateway.targetPort}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http", + "external": true + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http", + "external": true + }, + "orleans-silo": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 8000 + }, + "orleans-gateway": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 8001 + } + } + }, + "voting-redis-password": { + "type": "parameter.v0", + "value": "{voting-redis-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "special": false + } + } + } + } + }, + "voting-redis-password-uri-encoded": { + "type": "annotated.string", + "value": "{voting-redis-password.value}", + "filter": "uri" + }, + "voting-cluster-cluster-id": { + "type": "parameter.v0", + "value": "{voting-cluster-cluster-id.inputs.value}", + "inputs": { + "value": { + "type": "string", + "default": { + "generate": { + "minLength": 25, + "upper": false, + "special": false + } + } + } + } + }, + "voting-cluster-service-id": { + "type": "parameter.v0", + "value": "{voting-cluster-service-id.inputs.value}", + "inputs": { + "value": { + "type": "string", + "default": { + "generate": { + "minLength": 25, + "upper": false, + "special": false + } + } + } + } + }, + "cond-voting-redis-bindings-tcp-tlsenabled-2812f32c": { + "type": "value.v0", + "connectionString": "" + } + } +} \ No newline at end of file diff --git a/samples/volume-mount/VolumeMount.AppHost/aspire-manifest.json b/samples/volume-mount/VolumeMount.AppHost/aspire-manifest.json new file mode 100644 index 00000000..676981eb --- /dev/null +++ b/samples/volume-mount/VolumeMount.AppHost/aspire-manifest.json @@ -0,0 +1,118 @@ +{ + "resources": { + "sqlserver": { + "type": "container.v0", + "connectionString": "Server={sqlserver.bindings.tcp.host},{sqlserver.bindings.tcp.port};User ID=sa;Password={sqlserver-password.value};TrustServerCertificate=true", + "image": "mcr.microsoft.com/mssql/server:2022-latest", + "volumes": [ + { + "name": "volumemount.apphost--sqlserver-data", + "target": "/var/opt/mssql", + "readOnly": false + } + ], + "env": { + "ACCEPT_EULA": "Y", + "MSSQL_SA_PASSWORD": "{sqlserver-password.value}" + }, + "bindings": { + "tcp": { + "scheme": "tcp", + "protocol": "tcp", + "transport": "tcp", + "targetPort": 1433 + } + } + }, + "sqldb": { + "type": "value.v0", + "connectionString": "{sqlserver.connectionString};Initial Catalog=sqldb" + }, + "Storage": { + "type": "azure.bicep.v0", + "path": "Storage.module.bicep" + }, + "BlobConnection": { + "type": "value.v0", + "connectionString": "{Storage.outputs.blobEndpoint}" + }, + "migration": { + "type": "project.v0", + "path": "../VolumeMount.MigrationService/VolumeMount.MigrationService.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ConnectionStrings__sqldb": "{sqldb.connectionString}", + "SQLDB_HOST": "{sqlserver.bindings.tcp.host}", + "SQLDB_PORT": "{sqlserver.bindings.tcp.port}", + "SQLDB_USERNAME": "sa", + "SQLDB_PASSWORD": "{sqlserver-password.value}", + "SQLDB_URI": "mssql://sa:{sqlserver-password.value}@{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port}/sqldb", + "SQLDB_JDBCCONNECTIONSTRING": "jdbc:sqlserver://{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port};databaseName=sqldb;trustServerCertificate=true", + "SQLDB_DATABASENAME": "sqldb" + } + }, + "blazorweb": { + "type": "project.v0", + "path": "../VolumeMount.BlazorWeb/VolumeMount.BlazorWeb.csproj", + "env": { + "OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory", + "ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true", + "HTTP_PORTS": "{blazorweb.bindings.http.targetPort}", + "ConnectionStrings__sqldb": "{sqldb.connectionString}", + "SQLDB_HOST": "{sqlserver.bindings.tcp.host}", + "SQLDB_PORT": "{sqlserver.bindings.tcp.port}", + "SQLDB_USERNAME": "sa", + "SQLDB_PASSWORD": "{sqlserver-password.value}", + "SQLDB_URI": "mssql://sa:{sqlserver-password.value}@{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port}/sqldb", + "SQLDB_JDBCCONNECTIONSTRING": "jdbc:sqlserver://{sqlserver.bindings.tcp.host}:{sqlserver.bindings.tcp.port};databaseName=sqldb;trustServerCertificate=true", + "SQLDB_DATABASENAME": "sqldb", + "ConnectionStrings__BlobConnection": "{BlobConnection.connectionString}", + "BLOBCONNECTION_URI": "{Storage.outputs.blobEndpoint}" + }, + "bindings": { + "http": { + "scheme": "http", + "protocol": "tcp", + "transport": "http" + }, + "https": { + "scheme": "https", + "protocol": "tcp", + "transport": "http" + } + } + }, + "Storage-roles": { + "type": "azure.bicep.v0", + "path": "Storage-roles.module.bicep", + "params": { + "storage_outputs_name": "{Storage.outputs.name}", + "principalType": "", + "principalId": "" + } + }, + "sqlserver-password": { + "type": "parameter.v0", + "value": "{sqlserver-password.inputs.value}", + "inputs": { + "value": { + "type": "string", + "secret": true, + "default": { + "generate": { + "minLength": 22, + "minLower": 1, + "minUpper": 1, + "minNumeric": 1 + } + } + } + } + }, + "sqlserver-password-uri-encoded": { + "type": "annotated.string", + "value": "{sqlserver-password.value}", + "filter": "uri" + } + } +} \ No newline at end of file diff --git a/tests/SamplesIntegrationTests/ManifestTests.cs b/tests/SamplesIntegrationTests/ManifestTests.cs new file mode 100644 index 00000000..2aa2ecf8 --- /dev/null +++ b/tests/SamplesIntegrationTests/ManifestTests.cs @@ -0,0 +1,237 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using Xunit.Abstractions; + +namespace SamplesIntegrationTests; + +/// +/// Verifies that the Aspire publish manifest generated for each sample AppHost matches the +/// committed "expected" manifest (the aspire-manifest.json file in the AppHost project +/// directory). This catches unintended changes to the output of aspire publish. +/// +/// +/// When a change intentionally affects a manifest, regenerate the committed manifests by running +/// the tests locally with the ASPIRE_UPDATE_MANIFESTS environment variable set to +/// true, then review and commit the updated aspire-manifest.json files: +/// +/// PowerShell: $env:ASPIRE_UPDATE_MANIFESTS='true'; dotnet test --filter "FullyQualifiedName~ManifestTests" +/// bash: ASPIRE_UPDATE_MANIFESTS=true dotnet test --filter "FullyQualifiedName~ManifestTests" +/// +/// Update mode is intentionally disabled when running on CI. +/// +[Collection(ManifestTestsCollection.Name)] +public class ManifestTests(ITestOutputHelper testOutput) +{ + private static readonly TimeSpan GenerateTimeout = TimeSpan.FromMinutes(5); + + private const string ExpectedManifestFileName = "aspire-manifest.json"; + private const string GeneratedManifestFileName = "aspire-manifest.g.json"; + private const string VolumeHashPlaceholder = ""; + + // Files written next to the manifest as a side effect of publishing that must not be committed. + private static readonly string[] GeneratedArtifactPatterns = ["*.module.bicep", "*.Dockerfile"]; + + // Some samples bake ambient configuration values into resource environment variables (e.g. the + // Metrics sample's OpenTelemetry collector). Pin those inputs to deterministic values during + // generation so the manifests are reproducible regardless of the contributor's environment or + // user secrets, and so a real OTLP API key can never leak into a committed manifest. + private static readonly (string Key, string? Value)[] NeutralizedConfiguration = + [ + ("ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL", "http://localhost:18889"), + ("AppHost__OtlpApiKey", null), + ]; + + [Theory] + [MemberData(nameof(AppHostAssemblies))] + public async Task ManifestMatchesExpected(string appHostPath) + { + if (IsUpdateRequested && IsContinuousIntegration) + { + Assert.Fail($"'{UpdateEnvironmentVariable}' must not be set when running on CI; expected manifests can only be regenerated locally."); + } + + var appHostAssembly = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, appHostPath)); + var appHostType = appHostAssembly.GetTypes().FirstOrDefault(t => t.Name.EndsWith("_AppHost", StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"Could not find the AppHost entry point type in '{appHostPath}'."); + var appHostDir = GetAppHostDirectory(appHostAssembly); + var appPrefix = GetVolumeNamePrefix(appHostAssembly); + + var actualManifest = await GenerateManifestAsync(appHostType, appHostDir, appPrefix); + var expectedManifestPath = Path.Combine(appHostDir, ExpectedManifestFileName); + + if (IsUpdateRequested) + { + await File.WriteAllTextAsync(expectedManifestPath, actualManifest); + testOutput.WriteLine($"Updated expected manifest: {expectedManifestPath}"); + return; + } + + if (!File.Exists(expectedManifestPath)) + { + Assert.Fail($"No expected manifest was found at '{expectedManifestPath}'.{Environment.NewLine}{RegenerateInstructions}"); + } + + var expectedManifest = Normalize(await File.ReadAllTextAsync(expectedManifestPath), appPrefix); + + if (expectedManifest != actualManifest) + { + testOutput.WriteLine(RegenerateInstructions); + } + + Assert.Equal(expectedManifest, actualManifest); + } + + private async Task GenerateManifestAsync(Type appHostType, string appHostDir, string appPrefix) + { + // Snapshot the directory so cleanup only ever removes files this run created, never + // pre-existing source files that happen to match a generated-artifact pattern. + var preexistingFiles = Directory.GetFiles(appHostDir).ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Clear any artifacts left behind by a previously interrupted run before generating. + CleanGeneratedArtifacts(appHostDir, preexistingFiles); + + var generatedManifestPath = Path.Combine(appHostDir, GeneratedManifestFileName); + using var cts = new CancellationTokenSource(GenerateTimeout); + using var _ = NeutralizeConfiguration(); + + try + { + // Generate the manifest into the AppHost's own directory so that all relative paths + // (project references, bind mounts, generated bicep/Dockerfiles) are machine-independent. + // Publishing the manifest runs in-process and does not require Docker. + var builder = await DistributedApplicationTestingBuilder.CreateAsync( + appHostType, + ["--operation", "publish", "--publisher", "manifest", "--output-path", generatedManifestPath], + cts.Token); + + await using var app = await builder.BuildAsync(cts.Token); + await app.RunAsync(cts.Token); + + var manifest = await File.ReadAllTextAsync(generatedManifestPath, cts.Token); + return Normalize(manifest, appPrefix); + } + finally + { + CleanGeneratedArtifacts(appHostDir, preexistingFiles); + } + } + + private static void CleanGeneratedArtifacts(string appHostDir, HashSet preexistingFiles) + { + var generatedManifest = Path.Combine(appHostDir, GeneratedManifestFileName); + if (File.Exists(generatedManifest)) + { + File.Delete(generatedManifest); + } + + foreach (var pattern in GeneratedArtifactPatterns) + { + foreach (var file in Directory.GetFiles(appHostDir, pattern)) + { + if (!preexistingFiles.Contains(file)) + { + File.Delete(file); + } + } + } + } + + private static IDisposable NeutralizeConfiguration() + { + var previousValues = new (string Key, string? Value)[NeutralizedConfiguration.Length]; + for (var i = 0; i < NeutralizedConfiguration.Length; i++) + { + var (key, value) = NeutralizedConfiguration[i]; + previousValues[i] = (key, Environment.GetEnvironmentVariable(key)); + Environment.SetEnvironmentVariable(key, value); + } + + return new ConfigurationScope(previousValues); + } + + private sealed class ConfigurationScope((string Key, string? Value)[] previousValues) : IDisposable + { + public void Dispose() + { + foreach (var (key, value) in previousValues) + { + Environment.SetEnvironmentVariable(key, value); + } + } + } + + private static string Normalize(string manifest, string appPrefix) + { + // Volume names embed a 10-character SHA-256 prefix derived from the absolute AppHost path, + // which differs from machine to machine. Replace it with a stable placeholder. + if (!string.IsNullOrEmpty(appPrefix)) + { + manifest = Regex.Replace( + manifest, + Regex.Escape(appPrefix) + "-[0-9a-f]{10}-", + $"{appPrefix}-{VolumeHashPlaceholder}-"); + } + + // Normalize line endings so manifests compare equal regardless of git core.autocrlf settings. + return manifest.Replace("\r\n", "\n").Replace("\r", "\n"); + } + + private static string GetAppHostDirectory(Assembly appHostAssembly) + { + var path = appHostAssembly.GetCustomAttributes() + .FirstOrDefault(m => m.Key.Equals("apphostprojectpath", StringComparison.OrdinalIgnoreCase))?.Value; + + return !string.IsNullOrEmpty(path) && Directory.Exists(path) + ? path + : throw new InvalidOperationException( + $"Could not resolve the AppHost project directory for '{appHostAssembly.GetName().Name}'."); + } + + // Mirrors Aspire's VolumeNameGenerator: the volume name prefix is the sanitized, lower-cased application name. + private static string GetVolumeNamePrefix(Assembly appHostAssembly) + { + var applicationName = appHostAssembly.GetName().Name ?? string.Empty; + var builder = new StringBuilder(applicationName.Length); + + for (var i = 0; i < applicationName.Length; i++) + { + var c = applicationName[i]; + var isValid = i == 0 + ? char.IsAsciiLetter(c) || char.IsAsciiDigit(c) + : char.IsAsciiLetter(c) || char.IsAsciiDigit(c) || c is '_' or '.' or '-'; + builder.Append(isValid ? c : '_'); + } + + return builder.ToString().ToLowerInvariant(); + } + + public static TheoryData AppHostAssemblies() => + [.. Directory.GetFiles(AppContext.BaseDirectory, "*.AppHost.dll") + .Where(fileName => !fileName.EndsWith("Aspire.Hosting.AppHost.dll", StringComparison.OrdinalIgnoreCase)) + .Select(path => Path.GetRelativePath(AppContext.BaseDirectory, path))]; + + private const string UpdateEnvironmentVariable = "ASPIRE_UPDATE_MANIFESTS"; + + private static bool IsUpdateRequested => + string.Equals(Environment.GetEnvironmentVariable(UpdateEnvironmentVariable), "true", StringComparison.OrdinalIgnoreCase); + + private static bool IsContinuousIntegration => + string.Equals(Environment.GetEnvironmentVariable("CI"), "true", StringComparison.OrdinalIgnoreCase); + + private static string RegenerateInstructions => + $"To regenerate the expected manifests after an intentional change, run the tests locally with " + + $"the '{UpdateEnvironmentVariable}' environment variable set to 'true', then review and commit the " + + $"updated '{ExpectedManifestFileName}' files."; +} + +[CollectionDefinition(Name, DisableParallelization = true)] +public class ManifestTestsCollection +{ + // Manifests are generated into the AppHost source directories, so the tests must not run in + // parallel with each other or with other tests that touch those directories. + public const string Name = "Manifest tests"; +}