diff --git a/.github/actions/expand-tokens/Dockerfile b/.github/actions/expand-tokens/Dockerfile new file mode 100644 index 0000000..72a476f --- /dev/null +++ b/.github/actions/expand-tokens/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/powershell +WORKDIR /action + +RUN pwsh -c '$ProgressPreference = "SilentlyContinue"; Install-Module Axinom.DevOpsTooling -Scope AllUsers -AllowPrerelease -Force' + +COPY *.ps1 . + +ENTRYPOINT ["pwsh", "-c", "& /action/entrypoint.ps1"] \ No newline at end of file diff --git a/.github/actions/expand-tokens/action.yml b/.github/actions/expand-tokens/action.yml new file mode 100644 index 0000000..fba91f3 --- /dev/null +++ b/.github/actions/expand-tokens/action.yml @@ -0,0 +1,14 @@ +name: 'Expand tokens' +description: 'Replaces `__TOKENS__` with the values of equivalent environment variables in files.' +inputs: + path: + description: 'Path to the file or directory to process.' + required: true + filenames: + description: 'Filename filter, used if path is a directory.' + recursive: + description: 'Whether to recursively search for files, used if path is a directory.' + default: false +runs: + using: 'docker' + image: 'Dockerfile' \ No newline at end of file diff --git a/.github/actions/expand-tokens/entrypoint.ps1 b/.github/actions/expand-tokens/entrypoint.ps1 new file mode 100644 index 0000000..5a7800c --- /dev/null +++ b/.github/actions/expand-tokens/entrypoint.ps1 @@ -0,0 +1,7 @@ +$ErrorActionPreference = "Stop" + +Import-Module Axinom.DevOpsTooling + +$path = Join-Path $env:GITHUB_WORKSPACE $env:INPUT_PATH + +Expand-Tokens -path $path -filenames $env:INPUT_FILENAMES -recursive:([bool]$env:INPUT_RECURSIVE) \ No newline at end of file diff --git a/.github/actions/make-version-string/Dockerfile b/.github/actions/make-version-string/Dockerfile new file mode 100644 index 0000000..72a476f --- /dev/null +++ b/.github/actions/make-version-string/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/powershell +WORKDIR /action + +RUN pwsh -c '$ProgressPreference = "SilentlyContinue"; Install-Module Axinom.DevOpsTooling -Scope AllUsers -AllowPrerelease -Force' + +COPY *.ps1 . + +ENTRYPOINT ["pwsh", "-c", "& /action/entrypoint.ps1"] \ No newline at end of file diff --git a/.github/actions/make-version-string/action.yml b/.github/actions/make-version-string/action.yml new file mode 100644 index 0000000..6acbafd --- /dev/null +++ b/.github/actions/make-version-string/action.yml @@ -0,0 +1,12 @@ +name: 'Create the version string' +description: 'Generates a version string of the form [branch-]1.2.3-NNNNNNNNNNN-CCCCCCC, where N is an incrementing value and C identifies the commit, with an optional branch prefix. Fields lengths are unspecified and may increase in future versions.' +inputs: + assemblyInfoPath: + description: 'Path to a .NET style AssemblyInfo file containing the numeric version component.' + required: true +outputs: + versionstring: + description: 'The generated version string' +runs: + using: 'docker' + image: 'Dockerfile' \ No newline at end of file diff --git a/.github/actions/make-version-string/entrypoint.ps1 b/.github/actions/make-version-string/entrypoint.ps1 new file mode 100644 index 0000000..518394d --- /dev/null +++ b/.github/actions/make-version-string/entrypoint.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference = "Stop" + +Import-Module Axinom.DevOpsTooling + +$path = Join-Path $env:GITHUB_WORKSPACE $env:INPUT_ASSEMBLYINFOPATH + +$version = Set-DotNetBuildAndVersionStrings -assemblyInfoPath $path -commitId $ENV:GITHUB_SHA +Write-Host "::set-output name=versionstring::$version" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..da4f773 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - id: make_version_string + uses: ./.github/actions/make-version-string + with: + assemblyInfoPath: AssemblyInfo.cs + - uses: ./.github/actions/expand-tokens + env: + VERSIONSTRING: ${{ steps.make_version_string.outputs.versionstring }} + with: + path: Constants.cs +# - name: Publish to Registry +# uses: elgohr/Publish-Docker-Github-Action@master +# with: +# name: sandersaares/test123 +# username: sandersaares +# password: ${{ secrets.docker_hub_token }} diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..dff11dd --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Reflection; + +[assembly: AssemblyCompany("prometheus-net")] +[assembly: AssemblyProduct("Docker Stats Exporter")] +[assembly: AssemblyVersion("1.0.0")] \ No newline at end of file diff --git a/Constants.cs b/Constants.cs new file mode 100644 index 0000000..9a93cb1 --- /dev/null +++ b/Constants.cs @@ -0,0 +1,15 @@ +using System; + +namespace DockerExporter +{ + static class Constants + { + // Will be replaced with real version string (AssemblyInfo number + build parameters) on automated build. + public const string VersionString = "__VERSIONSTRING__"; + + /// + /// Docker can sometimes be slow to respond. If that is the case, we just give up and try again later. + /// + public static readonly TimeSpan DockerCommandTimeout = TimeSpan.FromSeconds(30); + } +} diff --git a/DockerExporter.csproj b/DockerExporter.csproj new file mode 100644 index 0000000..90cf006 --- /dev/null +++ b/DockerExporter.csproj @@ -0,0 +1,30 @@ + + + + Exe + netcoreapp3.1 + DockerExporter + false + preview + enable + + + + true + + true + + + + true + + true + + + + + + + + + diff --git a/DockerExporter.sln b/DockerExporter.sln new file mode 100644 index 0000000..64175a1 --- /dev/null +++ b/DockerExporter.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29609.76 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DockerExporter", "DockerExporter.csproj", "{A961E4EB-A444-4DCD-AE0F-48F5FDBFA88F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A961E4EB-A444-4DCD-AE0F-48F5FDBFA88F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A961E4EB-A444-4DCD-AE0F-48F5FDBFA88F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A961E4EB-A444-4DCD-AE0F-48F5FDBFA88F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A961E4EB-A444-4DCD-AE0F-48F5FDBFA88F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {645E4C22-DEE1-4A82-AD14-9C7036CA28C0} + EndGlobalSection +EndGlobal diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e097720 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build +WORKDIR /app + +# Separate layers here to avoid redoing dependencies on code change. +COPY *.sln . +COPY DockerExporter/*.csproj ./DockerExporter/ +RUN dotnet restore + +# Now the code. +COPY DockerExporter/. ./DockerExporter/ +WORKDIR /app/DockerExporter +RUN dotnet publish -c Release -o out + +FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS runtime +WORKDIR /app +COPY --from=build /app/DockerExporter/out ./ + +ENTRYPOINT ["dotnet", "DockerExporter.dll"] \ No newline at end of file diff --git a/ExporterLogic.cs b/ExporterLogic.cs new file mode 100644 index 0000000..a942e01 --- /dev/null +++ b/ExporterLogic.cs @@ -0,0 +1,47 @@ +using Axinom.Toolkit; +using Docker.DotNet; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DockerExporter +{ + public sealed class ExporterLogic + { + public string DockerUrl { get; set; } + + public ExporterLogic() + { + // Default value only valid if not running as container. + if (Helpers.Environment.IsMicrosoftOperatingSystem()) + { + DockerUrl = "npipe://./pipe/docker_engine"; + } + else + { + DockerUrl = "unix:///var/run/docker.sock"; + } + } + + public async Task RunAsync(CancellationToken cancel) + { + _log.Info($"Connecting to Docker via {DockerUrl}"); + + var clientConfig = new DockerClientConfiguration(new Uri(DockerUrl), null, Constants.DockerCommandTimeout); + + using (var client = clientConfig.CreateClient()) + { + var allContainers = await client.Containers.ListContainersAsync(new Docker.DotNet.Models.ContainersListParameters + { + All = true + }, cancel); + + _log.Info(Helpers.Debug.ToDebugString(allContainers)); + } + } + + private static readonly LogSource _log = Log.Default; + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..fb4f241 --- /dev/null +++ b/Program.cs @@ -0,0 +1,155 @@ +using Axinom.Toolkit; +using Mono.Options; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace DockerExporter +{ + internal sealed class Program + { + private readonly LogSource _log = Log.Default; + private readonly FilteringLogListener _filteringLogListener; + + private readonly ExporterLogic _logic = new ExporterLogic(); + + private void Run(string[] args) + { + // We signal this to shut down the service. + var cancel = new CancellationTokenSource(); + + try + { + if (!ParseArguments(args)) + { + Environment.ExitCode = -1; + return; + } + + _log.Info(GetVersionString()); + + // Control+C will gracefully shut us down. + Console.CancelKeyPress += (s, e) => + { + _log.Info("Canceling execution due to received signal."); + e.Cancel = true; + cancel.Cancel(); + }; + + _logic.RunAsync(cancel.Token).WaitAndUnwrapExceptions(); + + _log.Info("Application logic execution has completed."); + } + catch (OperationCanceledException) + { + if (cancel.IsCancellationRequested) + { + // We really were cancelled. That's fine. + } + else + { + _log.Error("Unexpected cancellation/timeout halted execution."); + } + + Environment.ExitCode = -1; + } + catch (AggregateException ex) + { + foreach (var innerException in ex.InnerExceptions) + { + _log.Error(innerException.Message); + _log.Error(innerException.GetType().Name); + } + + Environment.ExitCode = -1; + } + catch (Exception ex) + { + if (!string.IsNullOrWhiteSpace(ex.Message)) + { + _log.Error(ex.Message); + _log.Error(ex.GetType().Name); + } + + Environment.ExitCode = -1; + } + } + + private bool ParseArguments(string[] args) + { + var showHelp = false; + var verbose = false; + var debugger = false; + + var options = new OptionSet + { + GetVersionString(), + "", + "General", + { "h|?|help", "Displays usage instructions.", val => showHelp = val != null }, + { "docker-url=", "URL to use for accessing Docker.", val => _logic.DockerUrl = val }, + + "", + "Diagnostics", + { "verbose", "Displays extensive diagnostic information.", val => verbose = val != null }, + { "debugger", "Requests a debugger to be attached before execution starts.", val => debugger = val != null, true }, + }; + + List remainingOptions; + + try + { + remainingOptions = options.Parse(args); + + if (showHelp) + { + options.WriteOptionDescriptions(Console.Out); + return false; + } + + if (verbose) + _filteringLogListener.MinimumSeverity = LogEntrySeverity.Debug; + } + catch (OptionException ex) + { + Console.WriteLine(ex.Message); + Console.WriteLine("For usage instructions, use the --help command line parameter."); + return false; + } + + if (remainingOptions.Count != 0) + { + Console.WriteLine("Unknown command line parameters: {0}", string.Join(" ", remainingOptions.ToArray())); + Console.WriteLine("For usage instructions, use the --help command line parameter."); + return false; + } + + if (debugger) + Debugger.Launch(); + + return true; + } + + private string GetVersionString() + { + return $"{typeof(Program).Namespace} v{Constants.VersionString}"; + } + + private Program() + { + // We default to displaying Info or higher but allow this to be reconfiured later, if the user wishes. + _filteringLogListener = new FilteringLogListener(new ConsoleLogListener()) + { + MinimumSeverity = LogEntrySeverity.Info + }; + + Log.Default.RegisterListener(_filteringLogListener); + } + + private static void Main(string[] args) + { + new Program().Run(args); + } + } +}