diff --git a/docs/recipes/zipkin.yaml b/docs/recipes/zipkin.yaml new file mode 100644 index 00000000..1783fe2d --- /dev/null +++ b/docs/recipes/zipkin.yaml @@ -0,0 +1,40 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: zipkin + labels: + app.kubernetes.io/name: 'zipkin' +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: zipkin + template: + metadata: + labels: + app.kubernetes.io/name: 'zipkin' + spec: + containers: + - name: zipkin + image: openzipkin/zipkin + imagePullPolicy: Always + ports: + - containerPort: 9411 +... +--- +kind: Service +apiVersion: v1 +metadata: + name: zipkin + labels: + app.kubernetes.io/name: 'zipkin' +spec: + selector: + app.kubernetes.io/name: zipkin + type: ClusterIP + ports: + - name: http + protocol: TCP + port: 9411 + targetPort: 9411 +... \ No newline at end of file diff --git a/src/Microsoft.Tye.Core/CombineStep.cs b/src/Microsoft.Tye.Core/CombineStep.cs index 823f0b41..a6095fc3 100644 --- a/src/Microsoft.Tye.Core/CombineStep.cs +++ b/src/Microsoft.Tye.Core/CombineStep.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; @@ -49,12 +50,22 @@ namespace Microsoft.Tye // some duplication with the code in m8s (Application.cs) for populating environments. // // service.Service.Bindings is the bindings OUT - this step computes bindings IN. - var bindings = new ComputedBindings(); - service.Outputs.Add(bindings); + service.Outputs.Add(ComputeBindings(application, service.Dependencies)); + + foreach (var sidecar in project.Sidecars) + { + sidecar.Outputs.Add(ComputeBindings(application, sidecar.Dependencies)); + } + + return Task.CompletedTask; + } - foreach (var o in service.Dependencies) + private ComputedBindings ComputeBindings(ApplicationBuilder application, IEnumerable dependencies) + { + var bindings = new ComputedBindings(); + foreach (var dependency in dependencies) { - var other = application.Services.Single(a => a.Name == o); + var other = application.Services.Single(a => a.Name == dependency); foreach (var binding in other.Bindings) { @@ -126,7 +137,7 @@ namespace Microsoft.Tye } } - return Task.CompletedTask; + return bindings; } } } diff --git a/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs b/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs index fb549a5b..1776643c 100644 --- a/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs +++ b/src/Microsoft.Tye.Core/KubernetesManifestGenerator.cs @@ -274,6 +274,36 @@ namespace Microsoft.Tye spec = new YamlMappingNode(); template.Add("spec", spec); + if (project.Sidecars.Count > 0) + { + // Share process namespace when we have sidecars. So we can list other processes. + // see: https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace/#understanding-process-namespace-sharing + spec.Add("shareProcessNamespace", new YamlScalarNode("true") { Style = ScalarStyle.Plain }); + } + + if (project.RelocateDiagnosticsDomainSockets) + { + // Our diagnostics functionality uses $TMPDIR to locate other dotnet processes through + // eventpipe. see: https://github.com/dotnet/diagnostics/blob/master/documentation/design-docs/ipc-protocol.md#transport + // + // In order for diagnostics features to 'find' each other, we need to make $TMPDIR into + // something shared. + // + // see: https://kubernetes.io/docs/tasks/access-application-cluster/communicate-containers-same-pod-shared-volume/ + project.EnvironmentVariables.Add(new EnvironmentVariableBuilder("TMPDIR") + { + Value = "/var/tye/diagnostics", + }); + + foreach (var sidecar in project.Sidecars) + { + sidecar.EnvironmentVariables.Add(new EnvironmentVariableBuilder("TMPDIR") + { + Value = "/var/tye/diagnostics", + }); + } + } + var containers = new YamlSequenceNode(); spec.Add("containers", containers); @@ -292,7 +322,7 @@ namespace Microsoft.Tye project.Bindings.Any(b => b.Protocol == "http" || b.Protocol is null) || // We generate environment variables for other services if there dependencies - (bindings is object && bindings.Bindings.Any())) + bindings?.Bindings.Count > 0) { var env = new YamlSequenceNode(); container.Add("env", env); @@ -308,88 +338,21 @@ namespace Microsoft.Tye if (bindings is object) { - foreach (var binding in bindings.Bindings.OfType()) - { - env.Add(new YamlMappingNode() - { - { "name", binding.Name }, - { "value", new YamlScalarNode(binding.Value) { Style = ScalarStyle.SingleQuoted, } }, - }); - } - - foreach (var binding in bindings.Bindings.OfType()) - { - //- name: SECRET_USERNAME - // valueFrom: - // secretKeyRef: - // name: mysecret - // key: username + AddEnvironmentVariablesForComputedBindings(env, bindings); + } - if (binding is SecretConnectionStringInputBinding connectionStringBinding) - { - env.Add(new YamlMappingNode() - { - { "name", connectionStringBinding.KeyName }, - { "valueFrom", new YamlMappingNode() - { - { "secretKeyRef", new YamlMappingNode() - { - { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, - { "key", new YamlScalarNode("connectionstring") { Style = ScalarStyle.SingleQuoted } }, - } - }, - } - }, - }); - } - else if (binding is SecretUrlInputBinding urlBinding) - { - env.Add(new YamlMappingNode() - { - { "name", $"{urlBinding.KeyNameBase}__PROTOCOL" }, - { "valueFrom", new YamlMappingNode() - { - { "secretKeyRef", new YamlMappingNode() - { - { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, - { "key", new YamlScalarNode("protocol") { Style = ScalarStyle.SingleQuoted } }, - } - }, - } - }, - }); - - env.Add(new YamlMappingNode() - { - { "name", $"{urlBinding.KeyNameBase}__HOST" }, - { "valueFrom", new YamlMappingNode() - { - { "secretKeyRef", new YamlMappingNode() - { - { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, - { "key", new YamlScalarNode("host") { Style = ScalarStyle.SingleQuoted } }, - } - }, - } - }, - }); - - env.Add(new YamlMappingNode() - { - { "name", $"{urlBinding.KeyNameBase}__PORT" }, - { "valueFrom", new YamlMappingNode() - { - { "secretKeyRef", new YamlMappingNode() - { - { "name", new YamlScalarNode(binding.Name) { Style = ScalarStyle.SingleQuoted } }, - { "key", new YamlScalarNode("port") { Style = ScalarStyle.SingleQuoted } }, - } - }, - } - }, - }); - } - } + if (project.RelocateDiagnosticsDomainSockets) + { + // volumeMounts: + // - name: shared-data + // mountPath: /usr/share/nginx/html + var volumeMounts = new YamlSequenceNode(); + container.Add("volumeMounts", volumeMounts); + + var volumeMount = new YamlMappingNode(); + volumeMounts.Add(volumeMount); + volumeMount.Add("name", "tye-diagnostics"); + volumeMount.Add("mountPath", "/var/tye/diagnostics"); } } @@ -417,7 +380,129 @@ namespace Microsoft.Tye } } + foreach (var sidecar in project.Sidecars) + { + var container = new YamlMappingNode(); + containers.Add(container); + container.Add("name", sidecar.Name); // NOTE: to really support multiple images we'd need to generate unique names. + container.Add("image", $"{sidecar.ImageName}:{sidecar.ImageTag}"); + container.Add("imagePullPolicy", "Always"); // helps avoid problems with development + weak versioning + + if (sidecar.Args.Count > 0) + { + var args = new YamlSequenceNode(); + container.Add("args", args); + + foreach (var arg in sidecar.Args) + { + args.Add(new YamlScalarNode(arg) { Style = ScalarStyle.SingleQuoted, }); + } + } + + var sidecarBindings = sidecar.Outputs.OfType().FirstOrDefault(); + if (sidecar.EnvironmentVariables.Count > 0 || sidecarBindings?.Bindings.Count > 0) + { + var env = new YamlSequenceNode(); + container.Add("env", env); + + foreach (var kvp in sidecar.EnvironmentVariables) + { + env.Add(new YamlMappingNode() + { + { "name", kvp.Name }, + { "value", new YamlScalarNode(kvp.Value) { Style = ScalarStyle.SingleQuoted, } }, + }); + } + + if (sidecarBindings is object) + { + AddEnvironmentVariablesForComputedBindings(env, sidecarBindings); + } + } + + if (project.RelocateDiagnosticsDomainSockets) + { + // volumeMounts: + // - name: shared-data + // mountPath: /usr/share/nginx/html + var volumeMounts = new YamlSequenceNode(); + container.Add("volumeMounts", volumeMounts); + + var volumeMount = new YamlMappingNode(); + volumeMounts.Add(volumeMount); + volumeMount.Add("name", "tye-diagnostics"); + volumeMount.Add("mountPath", "/var/tye/diagnostics"); + } + } + + if (project.RelocateDiagnosticsDomainSockets) + { + // volumes: + // - name: shared-data + // emptyDir: {} + var volumes = new YamlSequenceNode(); + spec.Add("volumes", volumes); + + var volume = new YamlMappingNode(); + volumes.Add(volume); + volume.Add("name", "tye-diagnostics"); + volume.Add("emptyDir", new YamlMappingNode()); + } + return new KubernetesDeploymentOutput(project.Name, new YamlDocument(root)); } + + private static void AddEnvironmentVariablesForComputedBindings(YamlSequenceNode env, ComputedBindings bindings) + { + foreach (var binding in bindings.Bindings.OfType()) + { + env.Add(new YamlMappingNode() + { + { "name", binding.Name }, + { "value", new YamlScalarNode(binding.Value) { Style = ScalarStyle.SingleQuoted, } }, + }); + } + + foreach (var binding in bindings.Bindings.OfType()) + { + //- name: SECRET_USERNAME + // valueFrom: + // secretKeyRef: + // name: mysecret + // key: username + + if (binding is SecretConnectionStringInputBinding connectionStringBinding) + { + AddSecret(env, connectionStringBinding.KeyName, binding.Name, "connectionstring"); + + } + else if (binding is SecretUrlInputBinding urlBinding) + { + AddSecret(env, $"{urlBinding.KeyNameBase}__PROTOCOL", binding.Name, "protocol"); + AddSecret(env, $"{urlBinding.KeyNameBase}__HOST", binding.Name, "host"); + AddSecret(env, $"{urlBinding.KeyNameBase}__PORT", binding.Name, "port"); + } + } + + static void AddSecret(YamlSequenceNode env, string name, string secret, string key) + { + env.Add(new YamlMappingNode() + { + { "name", name }, + { + "valueFrom", new YamlMappingNode() + { + { + "secretKeyRef", new YamlMappingNode() + { + { "name", new YamlScalarNode(secret) { Style = ScalarStyle.SingleQuoted } }, + { "key", new YamlScalarNode(key) { Style = ScalarStyle.SingleQuoted } }, + } + }, + } + }, + }); + } + } } } diff --git a/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs index e38f2caf..46484751 100644 --- a/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs +++ b/src/Microsoft.Tye.Core/ProjectServiceBuilder.cs @@ -38,6 +38,7 @@ namespace Microsoft.Tye public string PublishDir { get; set; } = default!; public string IntermediateOutputPath { get; set; } = default!; public bool IsAspNet { get; set; } + public bool RelocateDiagnosticsDomainSockets { get; set; } // Data used for building containers public ContainerInfo? ContainerInfo { get; set; } @@ -51,5 +52,7 @@ namespace Microsoft.Tye public List Volumes { get; } = new List(); public Dictionary BuildProperties { get; } = new Dictionary(); + + public List Sidecars { get; } = new List(); } } diff --git a/src/Microsoft.Tye.Core/SidecarBuilder.cs b/src/Microsoft.Tye.Core/SidecarBuilder.cs new file mode 100644 index 00000000..d3530d74 --- /dev/null +++ b/src/Microsoft.Tye.Core/SidecarBuilder.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.Tye +{ + public class SidecarBuilder + { + public SidecarBuilder(string name, string imageName, string imageTag) + { + Name = name; + ImageName = imageName; + ImageTag = imageTag; + } + + public string Name { get; set; } + + public string ImageName { get; set; } + + public string ImageTag { get; set; } + + public List Args { get; } = new List(); + + public HashSet Dependencies { get; } = new HashSet(); + + public List Outputs { get; } = new List(); + + public List EnvironmentVariables { get; } = new List(); + } +} diff --git a/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs b/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs index 9c74880e..6a387ad1 100644 --- a/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs +++ b/src/Microsoft.Tye.Extensions/Zipkin/ZipkinExtension.cs @@ -14,50 +14,75 @@ namespace Microsoft.Tye.Extensions.Zipkin { public override Task ProcessAsync(ExtensionContext context, ExtensionConfiguration config) { - if (context.Operation == ExtensionContext.OperationKind.LocalRun) + if (context.Application.Services.Any(s => s.Name == "zipkin")) { - if (context.Application.Services.Any(s => s.Name == "zipkin")) - { - context.Output.WriteDebugLine("zipkin service already configured. Skipping..."); - } - else + context.Output.WriteDebugLine("zipkin service already configured. Skipping..."); + } + else + { + context.Output.WriteDebugLine("Injecting zipkin service..."); + var service = new ContainerServiceBuilder("zipkin", "openzipkin/zipkin") { - context.Output.WriteDebugLine("Injecting zipkin service..."); - - var service = new ContainerServiceBuilder("zipkin", "openzipkin/zipkin") + Bindings = { - Bindings = + new BindingBuilder() { - new BindingBuilder() - { - Port = 9411, - ContainerPort = 9411, - Protocol = "http", - }, + Port = 9411, + ContainerPort = 9411, + Protocol = "http", }, - }; - context.Application.Services.Add(service); + }, + }; + context.Application.Services.Add(service); - foreach (var s in context.Application.Services) + foreach (var s in context.Application.Services) + { + if (object.ReferenceEquals(s, service)) { - if (object.ReferenceEquals(s, service)) - { - continue; - } + continue; + } - // make zipkin available as a dependency of everything. - if (!s.Dependencies.Contains(service.Name)) - { - s.Dependencies.Add(service.Name); - } + // make zipkin available as a dependency of everything. + if (!s.Dependencies.Contains(service.Name)) + { + s.Dependencies.Add(service.Name); } } + } + if (context.Operation == ExtensionContext.OperationKind.LocalRun) + { if (context.Options!.DistributedTraceProvider is null) { + // For local development we hardcode the port and hostname context.Options.DistributedTraceProvider = "zipkin=http://localhost:9411"; } } + else if (context.Operation == ExtensionContext.OperationKind.Deploy) + { + foreach (var project in context.Application.Services.OfType()) + { + // Bring your rain boots. + project.RelocateDiagnosticsDomainSockets = true; + + var sidecar = new SidecarBuilder("tye-diag-agent", "rynowak/tye-diag-agent", "0.1") + { + Args = + { + "--kubernetes=true", + "--provider:0=zipkin=service:zipkin", + $"--service={project.Name}", + $"--assemblyName={project.AssemblyName}", + }, + Dependencies = + { + // Inject the zipkin service discovery variables + "zipkin", + }, + }; + project.Sidecars.Add(sidecar); + } + } return Task.CompletedTask; } diff --git a/src/Microsoft.Tye.Hosting.Diagnostics/LoggingSink.cs b/src/Microsoft.Tye.Hosting.Diagnostics/LoggingSink.cs index 334e53af..b14ee5f6 100644 --- a/src/Microsoft.Tye.Hosting.Diagnostics/LoggingSink.cs +++ b/src/Microsoft.Tye.Hosting.Diagnostics/LoggingSink.cs @@ -75,7 +75,7 @@ namespace Microsoft.Tye.Hosting.Diagnostics { argsJson = exceptionJson; exceptionJson = eventName; - eventName = null; + eventName = null!; } if (string.IsNullOrEmpty(argsJson)) diff --git a/src/tye-diag-agent/DiagnosticsMonitor.cs b/src/tye-diag-agent/DiagnosticsMonitor.cs new file mode 100644 index 00000000..5376f1f0 --- /dev/null +++ b/src/tye-diag-agent/DiagnosticsMonitor.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Tye.Hosting.Diagnostics; + +namespace Microsoft.Tye +{ + public class DiagnosticsMonitor : BackgroundService + { + private readonly ILogger logger; + private readonly IOptions options; + + public DiagnosticsMonitor(ILogger logger, IOptions options) + { + this.logger = logger; + this.options = options; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var collector = InitializeCollector(); + + var replicaInfo = new ReplicaInfo( + selector: processes => + { + // Find process by looking for the entry point dll in the arguments. + return processes.FirstOrDefault(p => + { + logger.LogDebug("Checking process {PID}.", p.Id); + + var command = GetCommand(p); + logger.LogDebug("Searching command '{Command}'.", command); + + return command.Contains($"{options.Value.AssemblyName}.dll"); + }); + }, + assemblyName: options.Value.AssemblyName, + service: options.Value.Service, + replica: options.Value.Kubernetes ? Dns.GetHostName() : options.Value.Service, + metrics: new ConcurrentDictionary()); + + while (!stoppingToken.IsCancellationRequested) + { + // The collector has a timeout in waiting for its filter to pass, so we + // won't burn the CPU to a crisp by doing this repetatively. + try + { + logger.LogInformation("Starting data collection"); + await collector.CollectAsync(replicaInfo, stoppingToken); + } + catch (Exception ex) when (!stoppingToken.IsCancellationRequested) + { + logger.LogError(ex, "Data collection threw an exception."); + } + } + } + + private DiagnosticsCollector InitializeCollector() + { + var collector = new DiagnosticsCollector(this.logger) + { + SelectProcessTimeout = TimeSpan.FromSeconds(60), + }; + + foreach (var provider in options.Value.Providers) + { + if (!DiagnosticsProvider.WellKnownProviders.TryGetValue(provider.Key, out var wellKnown)) + { + logger.LogError("Unknown provider type {Provider}. Skipping.", provider.Value); + continue; + } + + switch (wellKnown.Kind) + { + case DiagnosticsProvider.ProviderKind.Logging: + { + if (collector.LoggingSink is object) + { + logger.LogError("Logging is already initialized. Skipping."); + continue; + } + + logger.LogInformation(wellKnown.LogFormat, provider.Key); + collector.LoggingSink = new LoggingSink(logger, provider); + break; + } + + case DiagnosticsProvider.ProviderKind.Metrics: + { + if (collector.MetricSink is object) + { + logger.LogError("Metrics is already initialized. Skipping."); + continue; + } + + // TODO metrics + break; + } + + case DiagnosticsProvider.ProviderKind.Tracing: + { + if (collector.TracingSink is object) + { + logger.LogError("Tracing is already initialized. Skipping."); + continue; + } + + logger.LogInformation(wellKnown.LogFormat, provider.Value); + collector.TracingSink = new TracingSink(logger, provider); + break; + } + + default: + logger.LogError("Unknown provider type. Skipping."); + break; + } + } + + return collector; + } + + private static string GetCommand(Process target) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new PlatformNotSupportedException(); + } + + using var process = new Process() + { + StartInfo = + { + FileName = "ps", + Arguments = $"-p {target.Id.ToString(CultureInfo.InvariantCulture)} -o command=", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }, + EnableRaisingEvents = true, + }; + process.Start(); + process.WaitForExit(); + + return process.StandardOutput.ReadToEnd().Trim(); + } + } +} diff --git a/src/tye-diag-agent/DiagnosticsMonitorOptions.cs b/src/tye-diag-agent/DiagnosticsMonitorOptions.cs new file mode 100644 index 00000000..f2b9479e --- /dev/null +++ b/src/tye-diag-agent/DiagnosticsMonitorOptions.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.Tye +{ + public class DiagnosticsMonitorOptions + { + public bool Kubernetes { get; set; } + + public string Service { get; set; } = default!; + + public string AssemblyName { get; set; } = default!; + + public List Providers { get; } = new List(); + } +} diff --git a/src/tye-diag-agent/Dockerfile b/src/tye-diag-agent/Dockerfile new file mode 100644 index 00000000..3b380499 --- /dev/null +++ b/src/tye-diag-agent/Dockerfile @@ -0,0 +1,10 @@ +FROM mcr.microsoft.com/dotnet/core/runtime:3.1 +WORKDIR /app + +# We need `ps` +RUN apt-get update \ + && apt-get install -y procps \ + && rm -rf /var/lib/apt/lists/* + +COPY . . +ENTRYPOINT ["dotnet", "tye-diag-agent.dll"] \ No newline at end of file diff --git a/src/tye-diag-agent/Program.cs b/src/tye-diag-agent/Program.cs new file mode 100644 index 00000000..67d963e3 --- /dev/null +++ b/src/tye-diag-agent/Program.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.Tye +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + var configuration = hostContext.Configuration; + services.Configure(options => + { + options.Kubernetes = string.Equals(bool.TrueString, configuration["kubernetes"], StringComparison.OrdinalIgnoreCase); + options.AssemblyName = configuration["assemblyName"]; + options.Service = configuration["service"]; + + var section = configuration.GetSection("provider"); + foreach (var child in section.GetChildren()) + { + if (!DiagnosticsProvider.TryParse(child.Value, DiagnosticsProvider.ProviderKind.Unknown, out var provider)) + { + throw new InvalidOperationException("Could not parse diagnostics provider: " + child.Value); + } + + // The value used to connect to the service should be accessible with Tye service discovery. + if (provider.Value is string && provider.Value.StartsWith("service:")) + { + var service = provider.Value.Substring("service:".Length); + var value = configuration.GetServiceUri(service)?.AbsoluteUri ?? configuration.GetConnectionString(service); + provider = new DiagnosticsProvider(provider.Key, value, provider.Kind); + } + + options.Providers.Add(provider); + } + + if (options.Providers.Count == 0) + { + throw new InvalidOperationException("At least one provider must be configured."); + } + + if (string.IsNullOrEmpty(options.AssemblyName)) + { + throw new InvalidOperationException("The assembly name is required."); + } + + if (string.IsNullOrEmpty(options.Service)) + { + throw new InvalidOperationException("The service name is required."); + } + }); + services.AddHostedService(); + }); + } +} diff --git a/src/tye-diag-agent/Properties/launchSettings.json b/src/tye-diag-agent/Properties/launchSettings.json new file mode 100644 index 00000000..2572e4ca --- /dev/null +++ b/src/tye-diag-agent/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "tye_diag_agent": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/tye-diag-agent/appsettings.Development.json b/src/tye-diag-agent/appsettings.Development.json new file mode 100644 index 00000000..79c290dd --- /dev/null +++ b/src/tye-diag-agent/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning", + "Microsoft.Tye": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/tye-diag-agent/appsettings.json b/src/tye-diag-agent/appsettings.json new file mode 100644 index 00000000..b0862232 --- /dev/null +++ b/src/tye-diag-agent/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Tye": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/tye-diag-agent/docker.sh b/src/tye-diag-agent/docker.sh new file mode 100755 index 00000000..3c50a8d0 --- /dev/null +++ b/src/tye-diag-agent/docker.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +source="${BASH_SOURCE[0]}" + +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done + +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + +rm -rf "$scriptroot/../../artifacts/tye-diag-agent-publish" +dotnet publish "$scriptroot" \ + -c Release \ + -o "$scriptroot/../../artifacts/tye-diag-agent-publish" \ + -r linux-x64 --self-contained false + +if [ -z "$1" ]; then + docker build "$scriptroot/../../artifacts/tye-diag-agent-publish" -f "$scriptroot/Dockerfile" +else + docker build "$scriptroot/../../artifacts/tye-diag-agent-publish" -f "$scriptroot/Dockerfile" -t "rynowak/tye-diag-agent:$1" +fi \ No newline at end of file diff --git a/src/tye-diag-agent/tye-diag-agent.csproj b/src/tye-diag-agent/tye-diag-agent.csproj new file mode 100644 index 00000000..6278740f --- /dev/null +++ b/src/tye-diag-agent/tye-diag-agent.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp3.1 + Microsoft.Tye + + + + + + + + + + + diff --git a/tye.sln b/tye.sln index 0070be95..6cda0ced 100644 --- a/tye.sln +++ b/tye.sln @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Tye.Extensions.Co EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Infrastructure", "test\Test.Infrastructure\Test.Infrastructure.csproj", "{F9A7C801-26F3-4D20-B084-2A6A6343E869}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tye-diag-agent", "src\tye-diag-agent\tye-diag-agent.csproj", "{7173D2B6-B102-4DCA-8000-5DAE684BFC13}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -185,6 +187,18 @@ Global {F9A7C801-26F3-4D20-B084-2A6A6343E869}.Release|x64.Build.0 = Release|Any CPU {F9A7C801-26F3-4D20-B084-2A6A6343E869}.Release|x86.ActiveCfg = Release|Any CPU {F9A7C801-26F3-4D20-B084-2A6A6343E869}.Release|x86.Build.0 = Release|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Debug|x64.ActiveCfg = Debug|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Debug|x64.Build.0 = Debug|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Debug|x86.ActiveCfg = Debug|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Debug|x86.Build.0 = Debug|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Release|Any CPU.Build.0 = Release|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Release|x64.ActiveCfg = Release|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Release|x64.Build.0 = Release|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Release|x86.ActiveCfg = Release|Any CPU + {7173D2B6-B102-4DCA-8000-5DAE684BFC13}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -202,6 +216,7 @@ Global {7C9021B7-64BA-4DA9-88DA-5BC12A1C6233} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601} {FCE7C889-16D1-42E7-A514-EA096E9D41A7} = {F19B02EB-A372-417A-B2C2-EA0D5A3C76D5} {F9A7C801-26F3-4D20-B084-2A6A6343E869} = {F19B02EB-A372-417A-B2C2-EA0D5A3C76D5} + {7173D2B6-B102-4DCA-8000-5DAE684BFC13} = {8C662D59-A3CB-466F-8E85-A8E6BA5E7601} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D8002603-BB27-4500-BF86-274A8E72D302}