Ejemplo n.º 1
0
        public Tye.Hosting.Model.Application ToHostingApplication()
        {
            var services = new Dictionary <string, Tye.Hosting.Model.Service>();

            foreach (var service in Services)
            {
                RunInfo?runInfo;
                if (service.External)
                {
                    runInfo = null;
                }
                else if (service.DockerImage is object)
                {
                    runInfo = new DockerRunInfo(service.DockerImage, service.Args);
                }
                else if (service.Executable is object)
                {
                    runInfo = new ExecutableRunInfo(service.Executable, service.WorkingDirectory, service.Args);
                }
                else if (service.Project is object)
                {
                    runInfo = new ProjectRunInfo(service.Project, service.Args, service.Build ?? true);
                }
                else
                {
                    throw new InvalidOperationException($"Cannot figure out how to run service '{service.Name}'.");
                }

                var description = new Tye.Hosting.Model.ServiceDescription(service.Name, runInfo)
                {
                    Replicas = service.Replicas ?? 1,
                };

                foreach (var binding in service.Bindings)
                {
                    description.Bindings.Add(new Tye.Hosting.Model.ServiceBinding()
                    {
                        ConnectionString = binding.ConnectionString,
                        Host             = binding.Host,
                        AutoAssignPort   = binding.AutoAssignPort,
                        InternalPort     = binding.InternalPort,
                        Name             = binding.Name,
                        Port             = binding.Port,
                        Protocol         = binding.Protocol,
                    });
                }

                foreach (var entry in service.Configuration)
                {
                    description.Configuration.Add(new ConfigurationSource(entry.Name, entry.Value));
                }

                services.Add(service.Name, new Tye.Hosting.Model.Service(description));
            }

            return(new Tye.Hosting.Model.Application(Source, services));
        }
        private async Task TransformProjectToContainer(Model.Application application, Model.Service service, ProjectRunInfo project)
        {
            var serviceDescription = service.Description;
            var serviceName        = serviceDescription.Name;

            service.Status.ProjectFilePath = project.ProjectFile.FullName;
            var targetFramework = project.TargetFramework;

            // Sometimes building can fail because of file locking (like files being open in VS)
            _logger.LogInformation("Publishing project {ProjectFile}", service.Status.ProjectFilePath);

            var publishCommand = $"publish \"{service.Status.ProjectFilePath}\" --framework {targetFramework} /nologo";

            service.Logs.OnNext($"dotnet {publishCommand}");

            var buildResult = await ProcessUtil.RunAsync("dotnet", publishCommand, throwOnError : false);

            service.Logs.OnNext(buildResult.StandardOutput);

            if (buildResult.ExitCode != 0)
            {
                _logger.LogInformation("Publishing {ProjectFile} failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, service.Status.ProjectFilePath, buildResult.ExitCode);

                // Null out the RunInfo so that
                serviceDescription.RunInfo = null;
                return;
            }

            // We transform the project information into the following docker command:
            // docker run -w /app -v {publishDir}:/app -it {image} dotnet {outputfile}.dll

            // We swap the slashes since we're going to run on linux
            var containerImage = DetermineContainerImage(targetFramework);
            var outputFileName = project.AssemblyName + ".dll";
            var dockerRunInfo  = new DockerRunInfo(containerImage, $"dotnet {outputFileName} {project.Args}")
            {
                WorkingDirectory = "/app"
            };

            dockerRunInfo.VolumeMappings[project.PublishOutputPath] = "/app";

            // Make volume mapping works when running as a container
            foreach (var mapping in project.VolumeMappings)
            {
                dockerRunInfo.VolumeMappings[mapping.Key] = mapping.Value;
            }

            // Change the project into a container info
            serviceDescription.RunInfo = dockerRunInfo;
        }
        private async Task TransformProjectToContainer(Service service, ProjectRunInfo project)
        {
            var serviceDescription = service.Description;
            var serviceName        = serviceDescription.Name;

            service.Status.ProjectFilePath = project.ProjectFile.FullName;
            var targetFramework = project.TargetFramework;

            // Sometimes building can fail because of file locking (like files being open in VS)
            _logger.LogInformation("Publishing project {ProjectFile}", service.Status.ProjectFilePath);

            var buildArgs = project.BuildProperties.Aggregate(string.Empty, (current, property) => current + $" /p:{property.Key}={property.Value}").TrimStart();

            var publishCommand = $"publish \"{service.Status.ProjectFilePath}\" --framework {targetFramework} {buildArgs} /nologo";

            service.Logs.OnNext($"dotnet {publishCommand}");

            var buildResult = await ProcessUtil.RunAsync("dotnet", publishCommand, throwOnError : false);

            service.Logs.OnNext(buildResult.StandardOutput);

            if (buildResult.ExitCode != 0)
            {
                _logger.LogInformation("Publishing {ProjectFile} failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, service.Status.ProjectFilePath, buildResult.ExitCode);

                // Null out the RunInfo so that
                serviceDescription.RunInfo = null;
                return;
            }

            // We transform the project information into the following docker command:
            // docker run -w /app -v {publishDir}:/app -it {image} dotnet {outputfile}.dll

            var containerImage = DetermineContainerImage(project);
            var outputFileName = project.AssemblyName + ".dll";
            var dockerRunInfo  = new DockerRunInfo(containerImage, $"dotnet {outputFileName} {project.Args}")
            {
                WorkingDirectory = "/app"
            };

            dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: project.PublishOutputPath, name: null, target: "/app"));

            // Make volume mapping works when running as a container
            dockerRunInfo.VolumeMappings.AddRange(project.VolumeMappings);

            // Change the project into a container info
            serviceDescription.RunInfo = dockerRunInfo;
        }
Ejemplo n.º 4
0
        public Tye.Hosting.Model.Application ToHostingApplication()
        {
            var services = new Dictionary <string, Tye.Hosting.Model.Service>();

            foreach (var service in Services)
            {
                RunInfo?runInfo;
                if (service.External)
                {
                    runInfo = null;
                }
                else if (service.Image is object)
                {
                    var dockerRunInfo = new DockerRunInfo(service.Image, service.Args);

                    foreach (var mapping in service.Volumes)
                    {
                        dockerRunInfo.VolumeMappings[mapping.Source !] = mapping.Target !;
Ejemplo n.º 5
0
        public static Tye.Hosting.Model.Application ToHostingApplication(this ApplicationBuilder application)
        {
            var services = new Dictionary <string, Tye.Hosting.Model.Service>();

            foreach (var service in application.Services)
            {
                RunInfo?runInfo;
                int     replicas;
                var     env = new List <ConfigurationSource>();
                if (service is ExternalServiceBuilder)
                {
                    runInfo  = null;
                    replicas = 1;
                }
                else if (service is ContainerServiceBuilder container)
                {
                    var dockerRunInfo = new DockerRunInfo(container.Image, container.Args);

                    foreach (var mapping in container.Volumes)
                    {
                        dockerRunInfo.VolumeMappings[mapping.Source !] = mapping.Target !;
Ejemplo n.º 6
0
        private async Task TransformProjectToContainer(Model.Application application, Model.Service service, ProjectRunInfo project)
        {
            var serviceDescription = service.Description;
            var serviceName        = serviceDescription.Name;

            var expandedProject = Environment.ExpandEnvironmentVariables(project.Project);
            var fullProjectPath = Path.GetFullPath(Path.Combine(application.ContextDirectory, expandedProject));

            service.Status.ProjectFilePath = fullProjectPath;

            // Sometimes building can fail because of file locking (like files being open in VS)
            _logger.LogInformation("Publishing project {ProjectFile}", service.Status.ProjectFilePath);

            service.Logs.OnNext($"dotnet publish \"{service.Status.ProjectFilePath}\" /nologo");

            var buildResult = await ProcessUtil.RunAsync("dotnet", $"publish \"{service.Status.ProjectFilePath}\" /nologo", throwOnError : false);

            service.Logs.OnNext(buildResult.StandardOutput);

            if (buildResult.ExitCode != 0)
            {
                _logger.LogInformation("Publishing {ProjectFile} failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, service.Status.ProjectFilePath, buildResult.ExitCode);

                // Null out the RunInfo so that
                serviceDescription.RunInfo = null;
                return;
            }

            var targetFramework = GetTargetFramework(service.Status.ProjectFilePath);

            // We transform the project information into the following docker command:
            // docker run -w /app -v {projectDir}:/app -it {image} dotnet /app/bin/Debug/{tfm}/publish/{outputfile}.dll
            var containerImage = DetermineContainerImage(targetFramework);
            var outputFileName = Path.GetFileNameWithoutExtension(service.Status.ProjectFilePath) + ".dll";
            var dockerRunInfo  = new DockerRunInfo(containerImage, $"dotnet /app/bin/Debug/{targetFramework}/publish/{outputFileName} {project.Args}")
            {
                WorkingDirectory = "/app"
            };

            dockerRunInfo.VolumeMappings[Path.GetDirectoryName(service.Status.ProjectFilePath) !] = "/app";
Ejemplo n.º 7
0
        public static Application ToHostingApplication(this ApplicationBuilder application)
        {
            var services = new Dictionary <string, Service>();

            foreach (var service in application.Services)
            {
                RunInfo?runInfo;
                Probe?  liveness;
                Probe?  readiness;
                int     replicas;
                var     env = new List <EnvironmentVariable>();
                if (service is ExternalServiceBuilder)
                {
                    runInfo   = null;
                    liveness  = null;
                    readiness = null;
                    replicas  = 1;
                }
                else if (service is DockerFileServiceBuilder dockerFile)
                {
                    var dockerRunInfo = new DockerRunInfo(dockerFile.Image, dockerFile.Args)
                    {
                        IsAspNet  = dockerFile.IsAspNet,
                        BuildArgs = dockerFile.BuildArgs
                    };

                    if (!string.IsNullOrEmpty(dockerFile.DockerFile))
                    {
                        dockerRunInfo.DockerFile = new FileInfo(dockerFile.DockerFile);
                        if (!string.IsNullOrEmpty(dockerFile.DockerFileContext))
                        {
                            dockerRunInfo.DockerFileContext = new FileInfo(dockerFile.DockerFileContext);
                        }
                        else
                        {
                            dockerRunInfo.DockerFileContext = new FileInfo(dockerRunInfo.DockerFile.DirectoryName);
                        }
                    }

                    foreach (var mapping in dockerFile.Volumes)
                    {
                        dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target));
                    }

                    runInfo  = dockerRunInfo;
                    replicas = dockerFile.Replicas;
                    liveness = dockerFile.Liveness != null?GetProbeFromBuilder(dockerFile.Liveness) : null;

                    readiness = dockerFile.Readiness != null?GetProbeFromBuilder(dockerFile.Readiness) : null;

                    foreach (var entry in dockerFile.EnvironmentVariables)
                    {
                        env.Add(entry.ToHostingEnvironmentVariable());
                    }
                }

                else if (service is ContainerServiceBuilder container)
                {
                    var dockerRunInfo = new DockerRunInfo(container.Image, container.Args)
                    {
                        IsAspNet = container.IsAspNet
                    };

                    foreach (var mapping in container.Volumes)
                    {
                        dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target));
                    }

                    runInfo  = dockerRunInfo;
                    replicas = container.Replicas;
                    liveness = container.Liveness != null?GetProbeFromBuilder(container.Liveness) : null;

                    readiness = container.Readiness != null?GetProbeFromBuilder(container.Readiness) : null;

                    foreach (var entry in container.EnvironmentVariables)
                    {
                        env.Add(entry.ToHostingEnvironmentVariable());
                    }
                }
                else if (service is ExecutableServiceBuilder executable)
                {
                    runInfo  = new ExecutableRunInfo(executable.Executable, executable.WorkingDirectory, executable.Args);
                    replicas = executable.Replicas;
                    liveness = executable.Liveness != null?GetProbeFromBuilder(executable.Liveness) : null;

                    readiness = executable.Readiness != null?GetProbeFromBuilder(executable.Readiness) : null;

                    foreach (var entry in executable.EnvironmentVariables)
                    {
                        env.Add(entry.ToHostingEnvironmentVariable());
                    }
                }
                else if (service is DotnetProjectServiceBuilder project)
                {
                    if (project.TargetFrameworks.Length > 1)
                    {
                        throw new InvalidOperationException($"Unable to run {project.Name}. Multi-targeted projects are not supported.");
                    }

                    if (project.RunCommand == null)
                    {
                        throw new InvalidOperationException($"Unable to run {project.Name}. The project does not have a run command");
                    }

                    var projectInfo = new ProjectRunInfo(project);

                    foreach (var mapping in project.Volumes)
                    {
                        projectInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target));
                    }

                    runInfo  = projectInfo;
                    replicas = project.Replicas;
                    liveness = project.Liveness != null?GetProbeFromBuilder(project.Liveness) : null;

                    readiness = project.Readiness != null?GetProbeFromBuilder(project.Readiness) : null;

                    foreach (var entry in project.EnvironmentVariables)
                    {
                        env.Add(entry.ToHostingEnvironmentVariable());
                    }
                }
                else
                {
                    throw new InvalidOperationException($"Cannot figure out how to run service '{service.Name}'.");
                }

                var description = new ServiceDescription(service.Name, runInfo)
                {
                    Replicas  = replicas,
                    Liveness  = liveness,
                    Readiness = readiness
                };

                description.Configuration.AddRange(env);
                description.Dependencies.AddRange(service.Dependencies);

                foreach (var binding in service.Bindings)
                {
                    description.Bindings.Add(new ServiceBinding()
                    {
                        ConnectionString = binding.ConnectionString,
                        Host             = binding.Host,
                        ContainerPort    = binding.ContainerPort,
                        Name             = binding.Name,
                        Port             = binding.Port,
                        Protocol         = binding.Protocol,
                    });
                }

                services.Add(service.Name, new Service(description));
            }

            // Ingress get turned into services for hosting
            foreach (var ingress in application.Ingress)
            {
                var rules = new List <IngressRule>();

                foreach (var rule in ingress.Rules)
                {
                    rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service !));
                }

                var runInfo = new IngressRunInfo(rules);

                var description = new ServiceDescription(ingress.Name, runInfo)
                {
                    Replicas = ingress.Replicas,
                };

                foreach (var binding in ingress.Bindings)
                {
                    description.Bindings.Add(new ServiceBinding()
                    {
                        Name     = binding.Name,
                        Port     = binding.Port,
                        Protocol = binding.Protocol,
                    });
                }

                services.Add(ingress.Name, new Service(description));
            }

            return(new Application(application.Source, services)
            {
                Network = application.Network
            });
        }
Ejemplo n.º 8
0
        private async Task StartContainerAsync(Tye.Hosting.Model.Application application, Tye.Hosting.Model.Service service, DockerRunInfo docker)
        {
            if (!await DockerDetector.Instance.IsDockerInstalled.Value)
            {
                _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not installed.", service.Description.Name);

                service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not installed.");
                return;
            }

            if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value)
            {
                _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not running.", service.Description.Name);

                service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not running.");
                return;
            }

            var serviceDescription   = service.Description;
            var environmentArguments = "";
            var volumes          = "";
            var workingDirectory = docker.WorkingDirectory != null ? $"-w {docker.WorkingDirectory}" : "";

            // This is .NET specific
            var userSecretStore = GetUserSecretsPathFromSecrets();

            if (!string.IsNullOrEmpty(userSecretStore))
            {
                // Map the user secrets on this drive to user secrets
                docker.VolumeMappings[userSecretStore] = "/root/.microsoft/usersecrets:ro";
            }

            var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]);

            async Task RunDockerContainer(IEnumerable <(int Port, int?InternalPort, int BindingPort, string?Protocol)> ports)
            {
                var hasPorts = ports.Any();

                var replica = service.Description.Name.ToLower() + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower();
                var status  = new DockerStatus(service, replica);

                service.Replicas[replica] = status;

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status));

                var environment = new Dictionary <string, string>
                {
                    // Default to development environment
                    ["DOTNET_ENVIRONMENT"] = "Development",
                    // Remove the color codes from the console output
                    ["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"] = "true"
                };

                var portString = "";

                if (hasPorts)
                {
                    status.Ports = ports.Select(p => p.Port);

                    // These are the ports that the application should use for binding

                    // 1. Tell the docker container what port to bind to
                    portString = string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.InternalPort ?? p.Port}"));

                    // 2. Configure ASP.NET Core to bind to those same ports
                    environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.InternalPort ?? p.Port}"));

                    // Set the HTTPS port for the redirect middleware
                    foreach (var p in ports)
                    {
                        if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase))
                        {
                            // We need to set the redirect URL to the exposed port so the redirect works cleanly
                            environment["HTTPS_PORT"] = p.BindingPort.ToString();
                        }
                    }

                    // 3. For non-ASP.NET Core apps, pass the same information in the PORT env variable as a semicolon separated list.
                    environment["PORT"] = string.Join(";", ports.Select(p => $"{p.InternalPort ?? p.Port}"));
                }

                // See: https://github.com/docker/for-linux/issues/264
                //
                // The way we do proxying here doesn't really work for multi-container scenarios on linux
                // without some more setup.
                application.PopulateEnvironment(service, (key, value) => environment[key] = value, "host.docker.internal");

                environment["APP_INSTANCE"] = replica;

                foreach (var pair in environment)
                {
                    environmentArguments += $"-e {pair.Key}={pair.Value} ";
                }

                foreach (var pair in docker.VolumeMappings)
                {
                    var sourcePath = Path.GetFullPath(Path.Combine(application.ContextDirectory, pair.Key.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar)));
                    volumes += $"-v {sourcePath}:{pair.Value} ";
                }

                var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}";

                _logger.LogInformation("Running docker command {Command}", command);

                service.Logs.OnNext($"[{replica}]: {command}");

                status.DockerCommand = command;

                var result = await ProcessUtil.RunAsync(
                    "docker",
                    command,
                    throwOnError : false,
                    cancellationToken : dockerInfo.StoppingTokenSource.Token,
                    outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"));

                if (result.ExitCode != 0)
                {
                    _logger.LogError("docker run failed for {ServiceName} with exit code {ExitCode}:" + result.StandardError, service.Description.Name, result.ExitCode);
                    service.Replicas.TryRemove(replica, out _);
                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));

                    PrintStdOutAndErr(service, replica, result);
                    return;
                }

                var containerId = (string?)result.StandardOutput.Trim();

                // There's a race condition that sometimes makes us miss the output
                // so keep trying to get the container id
                while (string.IsNullOrEmpty(containerId))
                {
                    // Try to get the ID of the container
                    result = await ProcessUtil.RunAsync("docker", $"ps --no-trunc -f name={replica} --format " + "{{.ID}}");

                    containerId = result.ExitCode == 0 ? result.StandardOutput.Trim() : null;
                }

                var shortContainerId = containerId.Substring(0, Math.Min(12, containerId.Length));

                status.ContainerId = shortContainerId;

                _logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status));

                _logger.LogInformation("Collecting docker logs for {ContainerName}.", replica);

                await ProcessUtil.RunAsync("docker", $"logs -f {containerId}",
                                           outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                                           onStart : pid =>
                {
                    status.DockerLogsPid = pid;
                },
                                           throwOnError : false,
                                           cancellationToken : dockerInfo.StoppingTokenSource.Token);

                _logger.LogInformation("docker logs collection for {ContainerName} complete with exit code {ExitCode}", replica, result.ExitCode);

                // Docker has a tendency to get stuck so we're going to timeout this shutdown process
                var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

                _logger.LogInformation("Stopping container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                result = await ProcessUtil.RunAsync("docker", $"stop {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                PrintStdOutAndErr(service, replica, result);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status));

                _logger.LogInformation("Stopped container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                result = await ProcessUtil.RunAsync("docker", $"rm {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                PrintStdOutAndErr(service, replica, result);

                _logger.LogInformation("Removed container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                service.Replicas.TryRemove(replica, out _);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));
            };

            if (serviceDescription.Bindings.Count > 0)
            {
                // Each replica is assigned a list of internal ports, one mapped to each external
                // port
                for (var i = 0; i < serviceDescription.Replicas; i++)
                {
                    var ports = new List <(int, int?, int, string?)>();
                    foreach (var binding in serviceDescription.Bindings)
                    {
                        if (binding.Port == null)
                        {
                            continue;
                        }

                        ports.Add((service.PortMap[binding.Port.Value][i], binding.InternalPort, binding.Port.Value, binding.Protocol));
                    }

                    dockerInfo.Tasks[i] = RunDockerContainer(ports);
                }
            }
            else
            {
                for (var i = 0; i < service.Description.Replicas; i++)
                {
                    dockerInfo.Tasks[i] = RunDockerContainer(Enumerable.Empty <(int, int?, int, string?)>());
                }
            }

            service.Items[typeof(DockerInformation)] = dockerInfo;
        }
Ejemplo n.º 9
0
        public async Task StartAsync(Application application)
        {
            await PurgeFromPreviousRun();

            var containers = new List <Service>();

            foreach (var s in application.Services)
            {
                if (s.Value.Description.RunInfo is DockerRunInfo)
                {
                    containers.Add(s.Value);
                }
            }

            if (containers.Count == 0)
            {
                return;
            }

            var proxies = new List <Service>();

            foreach (var service in application.Services.Values)
            {
                if (service.Description.RunInfo is DockerRunInfo || service.Description.Bindings.Count == 0)
                {
                    continue;
                }

                // Inject a proxy per non-container service. This allows the container to use normal host names within the
                // container network to talk to services on the host
                var proxyContanier = new DockerRunInfo($"mcr.microsoft.com/dotnet/core/sdk:3.1", "dotnet Microsoft.Tye.Proxy.dll")
                {
                    WorkingDirectory = "/app",
                    NetworkAlias     = service.Description.Name,
                    Private          = true
                };
                var proxyLocation = Path.GetDirectoryName(typeof(Microsoft.Tye.Proxy.Program).Assembly.Location);
                proxyContanier.VolumeMappings.Add(new DockerVolume(proxyLocation, name: null, target: "/app"));
                var proxyDescription = new ServiceDescription($"{service.Description.Name}-proxy", proxyContanier);
                foreach (var binding in service.Description.Bindings)
                {
                    if (binding.Port == null)
                    {
                        continue;
                    }

                    var b = new ServiceBinding()
                    {
                        ConnectionString = binding.ConnectionString,
                        Host             = binding.Host,
                        ContainerPort    = binding.ContainerPort,
                        Name             = binding.Name,
                        Port             = binding.Port,
                        Protocol         = binding.Protocol
                    };
                    b.ReplicaPorts.Add(b.Port.Value);
                    proxyDescription.Bindings.Add(b);
                }
                var proxyContanierService = new Service(proxyDescription);
                containers.Add(proxyContanierService);
                proxies.Add(proxyContanierService);
            }

            string?dockerNetwork = null;

            if (!string.IsNullOrEmpty(application.Network))
            {
                var dockerNetworkResult = await ProcessUtil.RunAsync("docker", $"network ls --filter \"name={application.Network}\" --format \"{{{{.ID}}}}\"", throwOnError : false);

                if (dockerNetworkResult.ExitCode != 0)
                {
                    _logger.LogError("{Network}: Run docker network ls command failed", application.Network);

                    throw new CommandException("Run docker network ls command failed");
                }

                if (!string.IsNullOrWhiteSpace(dockerNetworkResult.StandardOutput))
                {
                    _logger.LogInformation("The specified network {Network} exists", application.Network);

                    dockerNetwork = application.Network;
                }
                else
                {
                    _logger.LogWarning("The specified network {Network} doesn't exist.", application.Network);

                    application.Network = null;
                }
            }

            // We're going to be making containers, only make a network if we have more than one (we assume they'll need to talk)
            if (string.IsNullOrEmpty(dockerNetwork) && containers.Count > 1)
            {
                dockerNetwork = "tye_network_" + Guid.NewGuid().ToString().Substring(0, 10);

                application.Items["dockerNetwork"] = dockerNetwork;

                _logger.LogInformation("Creating docker network {Network}", dockerNetwork);

                var command = $"network create --driver bridge {dockerNetwork}";

                _logger.LogInformation("Running docker command {Command}", command);

                var dockerNetworkResult = await ProcessUtil.RunAsync("docker", command, throwOnError : false);

                if (dockerNetworkResult.ExitCode != 0)
                {
                    _logger.LogInformation("Running docker command with exception info {ExceptionStdOut} {ExceptionStdErr}", dockerNetworkResult.StandardOutput, dockerNetworkResult.StandardError);

                    throw new CommandException("Run docker network create command failed");
                }
            }

            // Stash information outside of the application services
            application.Items[typeof(DockerApplicationInformation)] = new DockerApplicationInformation(dockerNetwork, proxies);

            var tasks = new Task[containers.Count];
            var index = 0;

            foreach (var s in containers)
            {
                var docker = (DockerRunInfo)s.Description.RunInfo !;

                tasks[index++] = StartContainerAsync(application, s, docker, dockerNetwork);
            }

            await Task.WhenAll(tasks);
        }
Ejemplo n.º 10
0
        private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker, string?dockerNetwork)
        {
            var serviceDescription   = service.Description;
            var environmentArguments = "";
            var volumes          = "";
            var workingDirectory = docker.WorkingDirectory != null ? $"-w {docker.WorkingDirectory}" : "";
            var hostname         = "host.docker.internal";
            var dockerImage      = docker.Image ?? service.Description.Name;

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                // See: https://github.com/docker/for-linux/issues/264
                //
                // host.docker.internal is making it's way into linux docker but doesn't work yet
                // instead we use the machine IP
                var addresses = await Dns.GetHostAddressesAsync(Dns.GetHostName());

                hostname = addresses[0].ToString();
            }

            async Task RunDockerContainer(IEnumerable <(int ExternalPort, int Port, int?ContainerPort, string?Protocol)> ports, CancellationToken cancellationToken)
            {
                var hasPorts = ports.Any();

                var replica = service.Description.Name.ToLower() + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower();
                var status  = new DockerStatus(service, replica);

                service.Replicas[replica] = status;

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status));

                var environment = new Dictionary <string, string>();

                var portString = "";

                if (hasPorts)
                {
                    status.Ports    = ports.Select(p => p.Port);
                    status.Bindings = ports.Select(p => new ReplicaBinding()
                    {
                        Port = p.Port, ExternalPort = p.ExternalPort, Protocol = p.Protocol
                    }).ToList();

                    // These are the ports that the application should use for binding

                    // 1. Tell the docker container what port to bind to
                    portString = docker.Private ? "" : string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.ContainerPort ?? p.Port}"));

                    if (docker.IsAspNet)
                    {
                        // 2. Configure ASP.NET Core to bind to those same ports
                        environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.ContainerPort ?? p.Port}"));

                        // Set the HTTPS port for the redirect middleware
                        foreach (var p in ports)
                        {
                            if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase))
                            {
                                // We need to set the redirect URL to the exposed port so the redirect works cleanly
                                environment["HTTPS_PORT"] = p.ExternalPort.ToString();
                            }
                        }
                    }

                    // 3. For non-ASP.NET Core apps, pass the same information in the PORT env variable as a semicolon separated list.
                    environment["PORT"] = string.Join(";", ports.Select(p => $"{p.ContainerPort ?? p.Port}"));

                    // This the port for the container proxy (containerport:externalport)
                    environment["PROXY_PORT"] = string.Join(";", ports.Select(p => $"{p.ContainerPort ?? p.Port}:{p.ExternalPort}"));
                }

                // See: https://github.com/docker/for-linux/issues/264
                //
                // The way we do proxying here doesn't really work for multi-container scenarios on linux
                // without some more setup.
                application.PopulateEnvironment(service, (key, value) => environment[key] = value, hostname !);

                environment["APP_INSTANCE"]   = replica;
                environment["CONTAINER_HOST"] = hostname !;

                status.Environment = environment;

                foreach (var pair in environment)
                {
                    environmentArguments += $"-e \"{pair.Key}={pair.Value}\" ";
                }

                foreach (var volumeMapping in docker.VolumeMappings)
                {
                    if (volumeMapping.Source != null)
                    {
                        var sourcePath = Path.GetFullPath(Path.Combine(application.ContextDirectory, volumeMapping.Source));
                        volumes += $"-v {sourcePath}:{volumeMapping.Target} ";
                    }
                    else if (volumeMapping.Name != null)
                    {
                        volumes += $"-v {volumeMapping.Name}:{volumeMapping.Target} ";
                    }
                }

                var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {dockerImage} {docker.Args ?? ""}";

                _logger.LogInformation("Running image {Image} for {Replica}", docker.Image, replica);

                service.Logs.OnNext($"[{replica}]: docker {command}");

                status.DockerCommand = command;
                status.DockerNetwork = dockerNetwork;

                WriteReplicaToStore(replica);
                var result = await ProcessUtil.RunAsync(
                    "docker",
                    command,
                    throwOnError : false,
                    cancellationToken : cancellationToken,
                    outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                    errorDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"));

                if (result.ExitCode != 0)
                {
                    _logger.LogError("docker run failed for {ServiceName} with exit code {ExitCode}:" + result.StandardError, service.Description.Name, result.ExitCode);
                    service.Replicas.TryRemove(replica, out var _);
                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));

                    PrintStdOutAndErr(service, replica, result);
                    return;
                }

                var containerId = (string?)result.StandardOutput.Trim();

                // There's a race condition that sometimes makes us miss the output
                // so keep trying to get the container id
                while (string.IsNullOrEmpty(containerId))
                {
                    // Try to get the ID of the container
                    result = await ProcessUtil.RunAsync("docker", $"ps --no-trunc -f name={replica} --format " + "{{.ID}}");

                    containerId = result.ExitCode == 0 ? result.StandardOutput.Trim() : null;
                }

                var shortContainerId = containerId.Substring(0, Math.Min(12, containerId.Length));

                status.ContainerId = shortContainerId;

                _logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                if (!string.IsNullOrEmpty(dockerNetwork))
                {
                    status.DockerNetworkAlias = docker.NetworkAlias ?? serviceDescription !.Name;

                    var networkCommand = $"network connect {dockerNetwork} {replica} --alias {status.DockerNetworkAlias}";

                    service.Logs.OnNext($"[{replica}]: docker {networkCommand}");

                    _logger.LogInformation("Running docker command {Command}", networkCommand);

                    result = await ProcessUtil.RunAsync("docker", networkCommand);

                    PrintStdOutAndErr(service, replica, result);
                }

                var sentStartedEvent = false;

                while (!cancellationToken.IsCancellationRequested)
                {
                    if (sentStartedEvent)
                    {
                        using var restartCts = new CancellationTokenSource(DockerStopTimeout);
                        result = await ProcessUtil.RunAsync("docker", $"restart {containerId}", throwOnError : false, cancellationToken : restartCts.Token);

                        if (restartCts.IsCancellationRequested)
                        {
                            _logger.LogWarning($"Failed to restart container after {DockerStopTimeout.Seconds} seconds.", replica, shortContainerId);
                            break; // implement retry mechanism?
                        }
                        else if (result.ExitCode != 0)
                        {
                            _logger.LogWarning($"Failed to restart container due to exit code {result.ExitCode}.", replica, shortContainerId);
                            break;
                        }

                        service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status));
                    }

                    using var stoppingCts      = new CancellationTokenSource();
                    status.StoppingTokenSource = stoppingCts;
                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status));
                    sentStartedEvent = true;

                    await using var _ = cancellationToken.Register(() => status.StoppingTokenSource.Cancel());

                    _logger.LogInformation("Collecting docker logs for {ContainerName}.", replica);

                    var backOff = TimeSpan.FromSeconds(5);

                    while (!status.StoppingTokenSource.Token.IsCancellationRequested)
                    {
                        var logsRes = await ProcessUtil.RunAsync("docker", $"logs -f {containerId}",
                                                                 outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                                                                 errorDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                                                                 throwOnError : false,
                                                                 cancellationToken : status.StoppingTokenSource.Token);

                        if (logsRes.ExitCode != 0)
                        {
                            break;
                        }

                        if (!status.StoppingTokenSource.IsCancellationRequested)
                        {
                            try
                            {
                                // Avoid spamming logs if restarts are happening
                                await Task.Delay(backOff, status.StoppingTokenSource.Token);
                            }
                            catch (OperationCanceledException)
                            {
                                break;
                            }
                        }

                        backOff *= 2;
                    }

                    _logger.LogInformation("docker logs collection for {ContainerName} complete with exit code {ExitCode}", replica, result.ExitCode);

                    status.StoppingTokenSource = null;
                }

                // Docker has a tendency to get stuck so we're going to timeout this shutdown process
                var timeoutCts = new CancellationTokenSource(DockerStopTimeout);

                _logger.LogInformation("Stopping container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                result = await ProcessUtil.RunAsync("docker", $"stop {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                if (timeoutCts.IsCancellationRequested)
                {
                    _logger.LogWarning($"Failed to stop container after {DockerStopTimeout.Seconds} seconds, container will most likely be running.", replica, shortContainerId);
                }

                PrintStdOutAndErr(service, replica, result);

                if (sentStartedEvent)
                {
                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status));
                }

                _logger.LogInformation("Stopped container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                result = await ProcessUtil.RunAsync("docker", $"rm {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                if (timeoutCts.IsCancellationRequested)
                {
                    _logger.LogWarning($"Failed to remove container after {DockerStopTimeout.Seconds} seconds, container will most likely still exist.", replica, shortContainerId);
                }

                PrintStdOutAndErr(service, replica, result);

                _logger.LogInformation("Removed container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                service.Replicas.TryRemove(replica, out var _);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));
            };

            async Task DockerBuildAsync(CancellationToken cancellationToken)
            {
                if (docker.DockerFile != null)
                {
                    _logger.LogInformation("Building docker image {Image} from docker file", dockerImage);

                    void Log(string data)
                    {
                        _logger.LogInformation("[" + serviceDescription !.Name + "]:" + data);
                        service.Logs.OnNext(data);
                    }

                    var dockerBuildResult = await ProcessUtil.RunAsync(
                        $"docker",
                        $"build \"{docker.DockerFileContext?.FullName}\" -t {dockerImage} -f \"{docker.DockerFile}\"",
                        outputDataReceived : Log,
                        errorDataReceived : Log,
                        workingDirectory : docker.WorkingDirectory,
                        cancellationToken : cancellationToken,
                        throwOnError : false);

                    if (dockerBuildResult.ExitCode != 0)
                    {
                        throw new CommandException("'docker build' failed.");
                    }
                }
            }

            Task DockerRunAsync(CancellationToken cancellationToken)
            {
                var tasks = new Task[serviceDescription !.Replicas];

                if (serviceDescription.Bindings.Count > 0)
                {
                    // Each replica is assigned a list of internal ports, one mapped to each external
                    // port
                    for (var i = 0; i < serviceDescription.Replicas; i++)
                    {
                        var ports = new List <(int, int, int?, string?)>();
                        foreach (var binding in serviceDescription.Bindings)
                        {
                            if (binding.Port == null)
                            {
                                continue;
                            }

                            ports.Add((binding.Port.Value, binding.ReplicaPorts[i], binding.ContainerPort, binding.Protocol));
                        }

                        tasks[i] = RunDockerContainer(ports, cancellationToken);
                    }
                }
                else
                {
                    for (var i = 0; i < service.Description.Replicas; i++)
                    {
                        tasks[i] = RunDockerContainer(Enumerable.Empty <(int, int, int?, string?)>(), cancellationToken);
                    }
                }

                return(Task.WhenAll(tasks));
            }
Ejemplo n.º 11
0
        private async Task TransformProjectToContainer(Service service, ProjectRunInfo project)
        {
            var serviceDescription = service.Description;
            var serviceName        = serviceDescription.Name;

            service.Status.ProjectFilePath = project.ProjectFile.FullName;
            var targetFramework = project.TargetFramework;

            // Sometimes building can fail because of file locking (like files being open in VS)
            _logger.LogInformation("Publishing project {ProjectFile}", service.Status.ProjectFilePath);

            var buildArgs = project.BuildProperties.Aggregate(string.Empty, (current, property) => current + $" /p:{property.Key}={property.Value}").TrimStart();

            var publishCommand = $"publish \"{service.Status.ProjectFilePath}\" --framework {targetFramework} {buildArgs} /nologo";

            service.Logs.OnNext($"dotnet {publishCommand}");

            var buildResult = await ProcessUtil.RunAsync("dotnet", publishCommand, throwOnError : false);

            service.Logs.OnNext(buildResult.StandardOutput);

            if (buildResult.ExitCode != 0)
            {
                _logger.LogInformation("Publishing {ProjectFile} failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, service.Status.ProjectFilePath, buildResult.ExitCode);

                // Null out the RunInfo so that
                serviceDescription.RunInfo = null;
                return;
            }

            // We transform the project information into the following docker command:
            // docker run -w /app -v {publishDir}:/app -it {image} dotnet {outputfile}.dll

            var containerImage = DetermineContainerImage(project);
            var outputFileName = project.AssemblyName + ".dll";
            var dockerRunInfo  = new DockerRunInfo(containerImage, $"dotnet {outputFileName} {project.Args}")
            {
                WorkingDirectory = "/app",
                IsAspNet         = project.IsAspNet
            };

            dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: project.PublishOutputPath, name: null, target: "/app"));

            // Make volume mapping works when running as a container
            dockerRunInfo.VolumeMappings.AddRange(project.VolumeMappings);

            // This is .NET specific
            var userSecretStore = GetUserSecretsPathFromSecrets();

            if (!string.IsNullOrEmpty(userSecretStore))
            {
                Directory.CreateDirectory(userSecretStore);

                // Map the user secrets on this drive to user secrets
                dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: userSecretStore, name: null, target: "/root/.microsoft/usersecrets", readOnly: true));
            }

            // Default to development environment
            serviceDescription.Configuration.Add(new EnvironmentVariable("DOTNET_ENVIRONMENT", "Development"));

            // Remove the color codes from the console output
            serviceDescription.Configuration.Add(new EnvironmentVariable("DOTNET_LOGGING__CONSOLE__DISABLECOLORS", "true"));

            if (project.IsAspNet)
            {
                serviceDescription.Configuration.Add(new EnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"));
                serviceDescription.Configuration.Add(new EnvironmentVariable("ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS", "true"));
            }

            // If we have an https binding then export the dev cert and mount the volume into the container
            if (serviceDescription.Bindings.Any(b => string.Equals(b.Protocol, "https", StringComparison.OrdinalIgnoreCase)))
            {
                // We export the developer certificate from this machine
                var certPassword         = Guid.NewGuid().ToString();
                var certificateDirectory = _certificateDirectory.Value;
                var certificateFilePath  = Path.Combine($"\"{certificateDirectory.DirectoryPath}", $"{project.AssemblyName}.pfx\"");
                await ProcessUtil.RunAsync("dotnet", $"dev-certs https -ep {certificateFilePath} -p {certPassword}");

                serviceDescription.Configuration.Add(new EnvironmentVariable("Kestrel__Certificates__Development__Password", certPassword));

                // Certificate Path: https://github.com/dotnet/aspnetcore/blob/a9d702624a02ad4ebf593d9bf9c1c69f5702a6f5/src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs#L419
                dockerRunInfo.VolumeMappings.Add(new DockerVolume(source: certificateDirectory.DirectoryPath, name: null, target: "/root/.aspnet/https", readOnly: true));
            }

            // Change the project into a container info
            serviceDescription.RunInfo = dockerRunInfo;
        }
Ejemplo n.º 12
0
        private async Task StartContainerAsync(Tye.Hosting.Model.Application application, Tye.Hosting.Model.Service service, DockerRunInfo docker)
        {
            if (!await DockerDetector.Instance.IsDockerInstalled.Value)
            {
                _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not installed.", service.Description.Name);

                service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not installed.");
                return;
            }

            if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value)
            {
                _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not running.", service.Description.Name);

                service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not running.");
                return;
            }

            var serviceDescription   = service.Description;
            var environmentArguments = "";

            var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]);

            async Task RunDockerContainer(IEnumerable <(int Port, int?InternalPort, int BindingPort, string?Protocol)> ports)
            {
                var hasPorts = ports.Any();

                var replica = service.Description.Name.ToLower() + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower();
                var status  = new DockerStatus(service, replica);

                service.Replicas[replica] = status;

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status));

                var environment = new Dictionary <string, string>
                {
                    // Default to development environment
                    ["DOTNET_ENVIRONMENT"] = "Development",
                    // Remove the color codes from the console output
                    ["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"] = "true"
                };

                var portString = "";

                if (hasPorts)
                {
                    status.Ports = ports.Select(p => p.Port);

                    portString = string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.InternalPort ?? p.Port}"));

                    foreach (var p in ports)
                    {
                        environment[$"{p.Protocol?.ToUpper() ?? "HTTP"}_PORT"] = p.BindingPort.ToString();
                    }
                }

                application.PopulateEnvironment(service, (key, value) => environment[key] = value, "host.docker.internal");

                environment["APP_INSTANCE"] = replica;

                foreach (var pair in environment)
                {
                    environmentArguments += $"-e {pair.Key}={pair.Value} ";
                }

                var command = $"run -d {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}";

                _logger.LogInformation("Running docker command {Command}", command);

                service.Logs.OnNext($"[{replica}]: {command}");

                status.DockerCommand = command;

                var result = await ProcessUtil.RunAsync(
                    "docker",
                    command,
                    throwOnError : false,
                    cancellationToken : dockerInfo.StoppingTokenSource.Token,
                    outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"));

                if (result.ExitCode != 0)
                {
                    _logger.LogError("docker run failed for {ServiceName} with exit code {ExitCode}:" + result.StandardError, service.Description.Name, result.ExitCode);
                    service.Replicas.TryRemove(replica, out _);
                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));

                    PrintStdOutAndErr(service, replica, result);
                    return;
                }

                var containerId = (string?)result.StandardOutput.Trim();

                // There's a race condition that sometimes makes us miss the output
                // so keep trying to get the container id
                while (string.IsNullOrEmpty(containerId))
                {
                    // Try to get the ID of the container
                    result = await ProcessUtil.RunAsync("docker", $"ps --no-trunc -f name={replica} --format " + "{{.ID}}");

                    containerId = result.ExitCode == 0 ? result.StandardOutput.Trim() : null;
                }

                var shortContainerId = containerId.Substring(0, Math.Min(12, containerId.Length));

                status.ContainerId = shortContainerId;

                _logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status));

                _logger.LogInformation("Collecting docker logs for {ContainerName}.", replica);

                await ProcessUtil.RunAsync("docker", $"logs -f {containerId}",
                                           outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                                           onStart : pid =>
                {
                    status.DockerLogsPid = pid;
                },
                                           throwOnError : false,
                                           cancellationToken : dockerInfo.StoppingTokenSource.Token);

                _logger.LogInformation("docker logs collection for {ContainerName} complete with exit code {ExitCode}", replica, result.ExitCode);

                // Docker has a tendency to get stuck so we're going to timeout this shutdown process
                var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

                _logger.LogInformation("Stopping container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                result = await ProcessUtil.RunAsync("docker", $"stop {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                PrintStdOutAndErr(service, replica, result);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status));

                _logger.LogInformation("Stopped container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                result = await ProcessUtil.RunAsync("docker", $"rm {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                PrintStdOutAndErr(service, replica, result);

                _logger.LogInformation("Removed container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                service.Replicas.TryRemove(replica, out _);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));
            };

            if (serviceDescription.Bindings.Count > 0)
            {
                // Each replica is assigned a list of internal ports, one mapped to each external
                // port
                for (var i = 0; i < serviceDescription.Replicas; i++)
                {
                    var ports = new List <(int, int?, int, string?)>();
                    foreach (var binding in serviceDescription.Bindings)
                    {
                        if (binding.Port == null)
                        {
                            continue;
                        }

                        ports.Add((service.PortMap[binding.Port.Value][i], binding.InternalPort, binding.Port.Value, binding.Protocol));
                    }

                    dockerInfo.Tasks[i] = RunDockerContainer(ports);
                }
            }
            else
            {
                for (var i = 0; i < service.Description.Replicas; i++)
                {
                    dockerInfo.Tasks[i] = RunDockerContainer(Enumerable.Empty <(int, int?, int, string?)>());
                }
            }

            service.Items[typeof(DockerInformation)] = dockerInfo;
        }
Ejemplo n.º 13
0
        public static Application ToHostingApplication(this ApplicationBuilder application)
        {
            var services = new Dictionary <string, Service>();

            foreach (var service in application.Services)
            {
                RunInfo?runInfo;
                int     replicas;
                var     env = new List <EnvironmentVariable>();
                if (service is ExternalServiceBuilder)
                {
                    runInfo  = null;
                    replicas = 1;
                }
                else if (service is ContainerServiceBuilder container)
                {
                    var dockerRunInfo = new DockerRunInfo(container.Image, container.Args);

                    foreach (var mapping in container.Volumes)
                    {
                        dockerRunInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target));
                    }

                    runInfo  = dockerRunInfo;
                    replicas = container.Replicas;

                    foreach (var entry in container.EnvironmentVariables)
                    {
                        env.Add(entry.ToHostingEnvironmentVariable());
                    }
                }
                else if (service is ExecutableServiceBuilder executable)
                {
                    runInfo  = new ExecutableRunInfo(executable.Executable, executable.WorkingDirectory, executable.Args);
                    replicas = executable.Replicas;

                    foreach (var entry in executable.EnvironmentVariables)
                    {
                        env.Add(entry.ToHostingEnvironmentVariable());
                    }
                }
                else if (service is ProjectServiceBuilder project)
                {
                    if (project.TargetFrameworks.Length > 1)
                    {
                        throw new InvalidOperationException($"Unable to run {project.Name}. Multi-targeted projects are not supported.");
                    }

                    if (project.RunCommand == null)
                    {
                        throw new InvalidOperationException($"Unable to run {project.Name}. The project does not have a run command");
                    }

                    var projectInfo = new ProjectRunInfo(project);

                    foreach (var mapping in project.Volumes)
                    {
                        projectInfo.VolumeMappings.Add(new DockerVolume(mapping.Source, mapping.Name, mapping.Target));
                    }

                    runInfo  = projectInfo;
                    replicas = project.Replicas;

                    foreach (var entry in project.EnvironmentVariables)
                    {
                        env.Add(entry.ToHostingEnvironmentVariable());
                    }
                }
                else
                {
                    throw new InvalidOperationException($"Cannot figure out how to run service '{service.Name}'.");
                }

                var description = new ServiceDescription(service.Name, runInfo)
                {
                    Replicas = replicas,
                };
                description.Configuration.AddRange(env);

                foreach (var binding in service.Bindings)
                {
                    description.Bindings.Add(new Hosting.Model.ServiceBinding()
                    {
                        ConnectionString = binding.ConnectionString,
                        Host             = binding.Host,
                        AutoAssignPort   = binding.AutoAssignPort,
                        ContainerPort    = binding.ContainerPort,
                        Name             = binding.Name,
                        Port             = binding.Port,
                        Protocol         = binding.Protocol,
                    });
                }

                services.Add(service.Name, new Service(description));
            }

            // Ingress get turned into services for hosting
            foreach (var ingress in application.Ingress)
            {
                var rules = new List <IngressRule>();

                foreach (var rule in ingress.Rules)
                {
                    rules.Add(new IngressRule(rule.Host, rule.Path, rule.Service !));
                }

                var runInfo = new IngressRunInfo(rules);

                var description = new ServiceDescription(ingress.Name, runInfo)
                {
                    Replicas = ingress.Replicas,
                };

                foreach (var binding in ingress.Bindings)
                {
                    description.Bindings.Add(new Hosting.Model.ServiceBinding()
                    {
                        AutoAssignPort = binding.AutoAssignPort,
                        Name           = binding.Name,
                        Port           = binding.Port,
                        Protocol       = binding.Protocol,
                    });
                }

                services.Add(ingress.Name, new Service(description));
            }

            return(new Application(application.Source, services));
        }
Ejemplo n.º 14
0
        private async Task StartContainerAsync(Application application, Service service, DockerRunInfo docker, string?dockerNetwork)
        {
            var serviceDescription   = service.Description;
            var environmentArguments = "";
            var volumes          = "";
            var workingDirectory = docker.WorkingDirectory != null ? $"-w {docker.WorkingDirectory}" : "";
            var hostname         = "host.docker.internal";

            if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
            {
                // See: https://github.com/docker/for-linux/issues/264
                //
                // host.docker.internal is making it's way into linux docker but doesn't work yet
                // instead we use the machine IP
                var addresses = await Dns.GetHostAddressesAsync(Dns.GetHostName());

                hostname = addresses[0].ToString();
            }

            // This is .NET specific
            var userSecretStore = GetUserSecretsPathFromSecrets();

            if (!string.IsNullOrEmpty(userSecretStore))
            {
                // Map the user secrets on this drive to user secrets
                docker.VolumeMappings.Add(new DockerVolume(source: userSecretStore, name: null, target: "/root/.microsoft/usersecrets:ro"));
            }

            var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]);

            async Task RunDockerContainer(IEnumerable <(int ExternalPort, int Port, int?ContainerPort, string?Protocol)> ports)
            {
                var hasPorts = ports.Any();

                var replica = service.Description.Name.ToLower() + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower();
                var status  = new DockerStatus(service, replica);

                service.Replicas[replica] = status;

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status));

                var environment = new Dictionary <string, string>
                {
                    // Default to development environment
                    ["DOTNET_ENVIRONMENT"]     = "Development",
                    ["ASPNETCORE_ENVIRONMENT"] = "Development",
                    // Remove the color codes from the console output
                    ["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"]     = "true",
                    ["ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS"] = "true"
                };

                var portString = "";

                if (hasPorts)
                {
                    status.Ports = ports.Select(p => p.Port);

                    // These are the ports that the application should use for binding

                    // 1. Tell the docker container what port to bind to
                    portString = string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.ContainerPort ?? p.Port}"));

                    // 2. Configure ASP.NET Core to bind to those same ports
                    environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.ContainerPort ?? p.Port}"));

                    // Set the HTTPS port for the redirect middleware
                    foreach (var p in ports)
                    {
                        if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase))
                        {
                            // We need to set the redirect URL to the exposed port so the redirect works cleanly
                            environment["HTTPS_PORT"] = p.ExternalPort.ToString();
                        }
                    }

                    // 3. For non-ASP.NET Core apps, pass the same information in the PORT env variable as a semicolon separated list.
                    environment["PORT"] = string.Join(";", ports.Select(p => $"{p.ContainerPort ?? p.Port}"));
                }

                // See: https://github.com/docker/for-linux/issues/264
                //
                // The way we do proxying here doesn't really work for multi-container scenarios on linux
                // without some more setup.
                application.PopulateEnvironment(service, (key, value) => environment[key] = value, hostname);

                environment["APP_INSTANCE"] = replica;

                status.Environment = environment;

                foreach (var pair in environment)
                {
                    environmentArguments += $"-e {pair.Key}={pair.Value} ";
                }

                foreach (var volumeMapping in docker.VolumeMappings)
                {
                    if (volumeMapping.Source != null)
                    {
                        var sourcePath = Path.GetFullPath(Path.Combine(application.ContextDirectory, volumeMapping.Source));
                        volumes += $"-v {sourcePath}:{volumeMapping.Target} ";
                    }
                    else if (volumeMapping.Name != null)
                    {
                        volumes += $"-v {volumeMapping.Name}:{volumeMapping.Target} ";
                    }
                }

                var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}";

                _logger.LogInformation("Running image {Image} for {Replica}", docker.Image, replica);

                service.Logs.OnNext($"[{replica}]: docker {command}");

                status.DockerCommand = command;
                status.DockerNetwork = dockerNetwork;

                WriteReplicaToStore(replica);
                var result = await ProcessUtil.RunAsync(
                    "docker",
                    command,
                    throwOnError : false,
                    cancellationToken : dockerInfo.StoppingTokenSource.Token,
                    outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                    errorDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"));

                if (result.ExitCode != 0)
                {
                    _logger.LogError("docker run failed for {ServiceName} with exit code {ExitCode}:" + result.StandardError, service.Description.Name, result.ExitCode);
                    service.Replicas.TryRemove(replica, out _);
                    service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));

                    PrintStdOutAndErr(service, replica, result);
                    return;
                }

                var containerId = (string?)result.StandardOutput.Trim();

                // There's a race condition that sometimes makes us miss the output
                // so keep trying to get the container id
                while (string.IsNullOrEmpty(containerId))
                {
                    // Try to get the ID of the container
                    result = await ProcessUtil.RunAsync("docker", $"ps --no-trunc -f name={replica} --format " + "{{.ID}}");

                    containerId = result.ExitCode == 0 ? result.StandardOutput.Trim() : null;
                }

                var shortContainerId = containerId.Substring(0, Math.Min(12, containerId.Length));

                status.ContainerId = shortContainerId;

                _logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                if (!string.IsNullOrEmpty(dockerNetwork))
                {
                    status.DockerNetworkAlias = serviceDescription.Name;

                    var networkCommand = $"network connect {dockerNetwork} {replica} --alias {serviceDescription.Name}";

                    service.Logs.OnNext($"[{replica}]: docker {networkCommand}");

                    _logger.LogInformation("Running docker command {Command}", networkCommand);

                    result = await ProcessUtil.RunAsync("docker", networkCommand);

                    PrintStdOutAndErr(service, replica, result);
                }

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status));

                _logger.LogInformation("Collecting docker logs for {ContainerName}.", replica);

                while (!dockerInfo.StoppingTokenSource.Token.IsCancellationRequested)
                {
                    var logsRes = await ProcessUtil.RunAsync("docker", $"logs -f {containerId}",
                                                             outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                                                             errorDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"),
                                                             throwOnError : false,
                                                             cancellationToken : dockerInfo.StoppingTokenSource.Token);

                    if (logsRes.ExitCode != 0)
                    {
                        break;
                    }

                    if (!dockerInfo.StoppingTokenSource.IsCancellationRequested)
                    {
                        try
                        {
                            // Avoid spamming logs if restarts are happening
                            await Task.Delay(5000, dockerInfo.StoppingTokenSource.Token);
                        }
                        catch (OperationCanceledException)
                        {
                            break;
                        }
                    }
                }

                _logger.LogInformation("docker logs collection for {ContainerName} complete with exit code {ExitCode}", replica, result.ExitCode);

                // Docker has a tendency to get stuck so we're going to timeout this shutdown process
                var timeoutCts = new CancellationTokenSource(DockerStopTimeout);

                _logger.LogInformation("Stopping container {ContainerName} with ID {ContainerId}", replica, shortContainerId);

                result = await ProcessUtil.RunAsync("docker", $"stop {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                if (timeoutCts.IsCancellationRequested)
                {
                    _logger.LogWarning($"Failed to stop container after {DockerStopTimeout.Seconds} seconds, container will most likely be running.", replica, shortContainerId);
                }

                PrintStdOutAndErr(service, replica, result);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status));

                _logger.LogInformation("Stopped container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                result = await ProcessUtil.RunAsync("docker", $"rm {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token);

                if (timeoutCts.IsCancellationRequested)
                {
                    _logger.LogWarning($"Failed to remove container after {DockerStopTimeout.Seconds} seconds, container will most likely still exist.", replica, shortContainerId);
                }

                PrintStdOutAndErr(service, replica, result);

                _logger.LogInformation("Removed container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode);

                service.Replicas.TryRemove(replica, out _);

                service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));
            };

            if (serviceDescription.Bindings.Count > 0)
            {
                // Each replica is assigned a list of internal ports, one mapped to each external
                // port
                for (var i = 0; i < serviceDescription.Replicas; i++)
                {
                    var ports = new List <(int, int, int?, string?)>();
                    foreach (var binding in serviceDescription.Bindings)
                    {
                        if (binding.Port == null)
                        {
                            continue;
                        }

                        ports.Add((binding.Port.Value, binding.ReplicaPorts[i], binding.ContainerPort, binding.Protocol));
                    }

                    dockerInfo.Tasks[i] = RunDockerContainer(ports);
                }
            }
            else
            {
                for (var i = 0; i < service.Description.Replicas; i++)
                {
                    dockerInfo.Tasks[i] = RunDockerContainer(Enumerable.Empty <(int, int, int?, string?)>());
                }
            }

            service.Items[typeof(DockerInformation)] = dockerInfo;
        }