diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4ffd13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,271 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +**/temp/** +artifacts/** +*.suo +*.user +*.userosscache +*.sln.docstates + +.tmp/** +.template.artifacts/** + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +/.dotnet +/tools/* +!/tools/packages.config +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + + diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..3244650 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,127 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "ApplicationDirectory": { + "type": "string", + "description": "Application directory against which buildpack will be applied" + }, + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)" + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "GitHubToken": { + "type": "string", + "description": "GitHub personal access token with access to the repo" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Detect", + "Publish", + "Release", + "Supply" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Stack": { + "type": "string", + "description": "Target CF stack type - 'windows' or 'linux'. Determines buildpack runtime (Framework or Core). Default is both", + "enum": [ + "Linux", + "Windows" + ] + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "Detect", + "Publish", + "Release", + "Supply" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..f69b07c --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "KerberosBuildpack.sln" +} \ No newline at end of file diff --git a/KerberosBuildpack.sln b/KerberosBuildpack.sln new file mode 100644 index 0000000..3a3097b --- /dev/null +++ b/KerberosBuildpack.sln @@ -0,0 +1,80 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KerberosBuildpack", "src\KerberosBuildpack\KerberosBuildpack.csproj", "{512F3F46-B79C-40C7-B148-7E1DDDE3DA54}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{F6E63DDE-EC41-4756-A6A3-7BF6A3ACC172}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lifecycle.Detect", "src\Lifecycle.Detect\Lifecycle.Detect.csproj", "{A22F5B71-D4F4-46AC-A5B3-C90937296054}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LifecycleHooks", "LifecycleHooks", "{A64D29F8-0B69-41EE-915E-0C3F74DCFDB3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lifecycle.Finalize", "src\Lifecycle.Finalize\Lifecycle.Finalize.csproj", "{FB233232-09B8-40E6-9239-BABAE73D2C01}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lifecycle.Release", "src\Lifecycle.Release\Lifecycle.Release.csproj", "{44E45D83-08DA-4155-A7E8-7F16B0692B59}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lifecycle.Supply", "src\Lifecycle.Supply\Lifecycle.Supply.csproj", "{6A6060AB-FA8F-4A37-A6D4-CCF2FF5FA54E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7BF14C44-4B4C-4973-B7AD-1C2D45242AE2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{A989B8EF-B8C1-4BA9-BF91-B3B0529E4584}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KerberosTicketRefresher", "src\KerberosTicketRefresher\KerberosTicketRefresher.csproj", "{6D05C4AD-EE88-4359-8BCD-02B5C32CB26C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KerberosCommon", "src\KerberosCommon\KerberosCommon.csproj", "{93725672-D00B-4C69-84A9-38DF83B368BD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F6E63DDE-EC41-4756-A6A3-7BF6A3ACC172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6E63DDE-EC41-4756-A6A3-7BF6A3ACC172}.Release|Any CPU.ActiveCfg = Release|Any CPU + {512F3F46-B79C-40C7-B148-7E1DDDE3DA54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {512F3F46-B79C-40C7-B148-7E1DDDE3DA54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {512F3F46-B79C-40C7-B148-7E1DDDE3DA54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {512F3F46-B79C-40C7-B148-7E1DDDE3DA54}.Release|Any CPU.Build.0 = Release|Any CPU + {A22F5B71-D4F4-46AC-A5B3-C90937296054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A22F5B71-D4F4-46AC-A5B3-C90937296054}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A22F5B71-D4F4-46AC-A5B3-C90937296054}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A22F5B71-D4F4-46AC-A5B3-C90937296054}.Release|Any CPU.Build.0 = Release|Any CPU + {FB233232-09B8-40E6-9239-BABAE73D2C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB233232-09B8-40E6-9239-BABAE73D2C01}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB233232-09B8-40E6-9239-BABAE73D2C01}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB233232-09B8-40E6-9239-BABAE73D2C01}.Release|Any CPU.Build.0 = Release|Any CPU + {44E45D83-08DA-4155-A7E8-7F16B0692B59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44E45D83-08DA-4155-A7E8-7F16B0692B59}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44E45D83-08DA-4155-A7E8-7F16B0692B59}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44E45D83-08DA-4155-A7E8-7F16B0692B59}.Release|Any CPU.Build.0 = Release|Any CPU + {6A6060AB-FA8F-4A37-A6D4-CCF2FF5FA54E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A6060AB-FA8F-4A37-A6D4-CCF2FF5FA54E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A6060AB-FA8F-4A37-A6D4-CCF2FF5FA54E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A6060AB-FA8F-4A37-A6D4-CCF2FF5FA54E}.Release|Any CPU.Build.0 = Release|Any CPU + {6D05C4AD-EE88-4359-8BCD-02B5C32CB26C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D05C4AD-EE88-4359-8BCD-02B5C32CB26C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D05C4AD-EE88-4359-8BCD-02B5C32CB26C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D05C4AD-EE88-4359-8BCD-02B5C32CB26C}.Release|Any CPU.Build.0 = Release|Any CPU + {93725672-D00B-4C69-84A9-38DF83B368BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93725672-D00B-4C69-84A9-38DF83B368BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93725672-D00B-4C69-84A9-38DF83B368BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93725672-D00B-4C69-84A9-38DF83B368BD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {562B0D1A-9FD1-4FE2-AA87-CA7A9C5D4E89} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {A22F5B71-D4F4-46AC-A5B3-C90937296054} = {A64D29F8-0B69-41EE-915E-0C3F74DCFDB3} + {FB233232-09B8-40E6-9239-BABAE73D2C01} = {A64D29F8-0B69-41EE-915E-0C3F74DCFDB3} + {44E45D83-08DA-4155-A7E8-7F16B0692B59} = {A64D29F8-0B69-41EE-915E-0C3F74DCFDB3} + {6A6060AB-FA8F-4A37-A6D4-CCF2FF5FA54E} = {A64D29F8-0B69-41EE-915E-0C3F74DCFDB3} + {A64D29F8-0B69-41EE-915E-0C3F74DCFDB3} = {7BF14C44-4B4C-4973-B7AD-1C2D45242AE2} + {512F3F46-B79C-40C7-B148-7E1DDDE3DA54} = {7BF14C44-4B4C-4973-B7AD-1C2D45242AE2} + {F6E63DDE-EC41-4756-A6A3-7BF6A3ACC172} = {A989B8EF-B8C1-4BA9-BF91-B3B0529E4584} + {6D05C4AD-EE88-4359-8BCD-02B5C32CB26C} = {7BF14C44-4B4C-4973-B7AD-1C2D45242AE2} + {93725672-D00B-4C69-84A9-38DF83B368BD} = {7BF14C44-4B4C-4973-B7AD-1C2D45242AE2} + EndGlobalSection +EndGlobal diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..314170a --- /dev/null +++ b/README.MD @@ -0,0 +1,73 @@ +This project offers simplified way of creating buildpacks for Cloud Foundry in .NET + +Buildpacks fall into two categories: supply and final. Supply buildpacks can be chained to supply dependencies to the app or modify its configuration. They essentially act as middleware. Final buildpack is the one that is launched in the end of the buildpack chain and is responsible for telling Cloud Foundry the startup command for the application. + +## Quick Start + +This is a template project that can be used by DotNet CLI to boostrap a new buildpack. Install it into CLI via + +```shell +dotnet new -i CloudFoundry.Buildpack.V2 +``` + +The embedded build scripts rely on Git to do versioning through the use of [GitVersion](https://gitversion.readthedocs.io/en/latest/). This means that you need to initialize a git repo and create at least one commit + +```shell +git init +git add . +git commit -m Initial +``` + +#### How to implement + +Start with `MyBuilpack` class. Depending on the type of buildpack you're creating, inherit either from `SupplyBuildpack` or `FinalBuildpack` and implement the `Detect`, `Apply`, and in the case of final buildpack the `GetStartupCommand`. + +## How to package + +Included build packaging is based on Nuke.Build project. Either use the included build shell scripts, or install IDE plugins to run included targets. Get full list of useful targets and parameters with `--help` argument for the build script. + +You can build buildpack that are compatible with both Windows and Linux stacks, however when targeting Windows stack it can only be compiled on a Windows machine. + +The provided build script accepts one argument to specify the stack you're targeting. Windows stack will leverage .NET Framework (which is already included in the stemcell), resulting in smaller buildpack. When targeting Linux, the buildpack will be assembled with .NET core as self-contained (resulting in ~22mb package). + +### Compiling on Windows + +```powershell +.\build.ps1 --stack windows +``` + +or + +```powershell +.\build.ps1 --stack linux +``` + +### Compiling on Linux or Mac + +```bash +./build.sh --stack linux +``` + +Final output will be placed in `/artifacts` folder + +## How to Release + +Use the embedded `Release` target to create and publish buildpack as asset to GitHub releases. This requires you have remote repo set and have specified API Key as parameter (or set via environmental variable). + +## How to use + +* Option 1: Upload to Cloud Foundry via `cf create-buildpack` option and reference in manifest by name +* Option 2: Upload to a public host (like GitHub releases page) and reference in manifest via URL + +#### Sample manifest + + +```yaml +applications: +- name: simpleapp + stack: windows2016 + buildpacks: + - https://github.com/macsux/web-config-transform-buildpack/releases/download/1.0/web-config-transform-buildpack.zip + - hwc_buildpack +``` + diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..cb7fbdb --- /dev/null +++ b/build.ps1 @@ -0,0 +1,68 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "Windows PowerShell $($Host.Version)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.tmp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" +$DotNetChannel = "Current" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If global.json exists, load expected version +if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } +} + +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if ((Get-Command "dotnet" -ErrorAction SilentlyContinue) -ne $null -and ` + (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + $DotNetDirectory = "$TempDirectory\dotnet-win" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + md -force $TempDirectory > $null + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # Install by channel or version + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } +} + +Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..6d6e2ad --- /dev/null +++ b/build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +echo $(bash --version 2>&1 | head -n 1) + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" +DOTNET_CHANNEL="Current" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} +} + +# If global.json exists, load expected version +if [ -f "$DOTNET_GLOBAL_FILE" ]; then + DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE")) + if [ "$DOTNET_VERSION" == "" ]; then + unset DOTNET_VERSION + fi +fi + +# If dotnet is installed locally, and expected version is not set or installation matches the expected version +if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then + export DOTNET_EXE="$(command -v dotnet)" +else + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # Install by channel or version + if [ -z ${DOTNET_VERSION+x} ]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi +fi + +echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 0000000..94be682 --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,10 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Build.cs b/build/Build.cs new file mode 100644 index 0000000..333c846 --- /dev/null +++ b/build/Build.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.CompilerServices; +using ICSharpCode.SharpZipLib.Zip; +using NuGet.Configuration; +using Nuke.Common; +using Nuke.Common.Execution; +using Nuke.Common.Git; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitHub; +using Nuke.Common.Tools.GitVersion; +using Nuke.Common.Utilities.Collections; +using Octokit; +using static Nuke.Common.IO.FileSystemTasks; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using FileMode = System.IO.FileMode; +using ZipFile = System.IO.Compression.ZipFile; + +[assembly: InternalsVisibleTo("KerberosBuildpackTests")] +[CheckBuildProjectConfigurations] +[UnsetVisualStudioEnvironmentVariables] +class Build : NukeBuild +{ + /// Support plugins are available for: + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - Microsoft VSCode https://nuke.build/vscode + + [Flags] + public enum StackType + { + Windows = 1, + Linux = 2 + } + public static int Main () => Execute(x => x.Publish); + const string BuildpackProjectName = "KerberosBuildpack"; + string GetPackageZipName(string runtime) => $"{BuildpackProjectName}-{runtime}-{GitVersion.MajorMinorPatch}.zip"; + + + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly string Configuration = "Debug"; + + [Parameter("Target CF stack type - 'windows' or 'linux'. Determines buildpack runtime (Framework or Core). Default is both")] + readonly StackType Stack = StackType.Linux; + + [Parameter("GitHub personal access token with access to the repo")] + string GitHubToken; + + [Parameter("Application directory against which buildpack will be applied")] + readonly string ApplicationDirectory; + + IEnumerable PublishCombinations + { + get + { + if (Stack.HasFlag(StackType.Windows)) + yield return new PublishTarget {Framework = "net472", Runtime = "win-x64"}; + if (Stack.HasFlag(StackType.Linux)) + yield return new PublishTarget {Framework = "net6.0", Runtime = "linux-x64"}; + } + } + + [Solution] readonly Solution Solution; + [GitRepository] readonly GitRepository GitRepository; + [GitVersion] readonly GitVersion GitVersion; + + AbsolutePath SourceDirectory => RootDirectory / "src"; + AbsolutePath TestsDirectory => RootDirectory / "tests"; + AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; + + string[] LifecycleHooks = {"detect", "supply", "release", "finalize"}; + + Target Clean => _ => _ + .Description("Cleans up **/bin and **/obj folders") + .Executes(() => + { + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); + TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); + }); + + Target Compile => _ => _ + .Description("Compiles the buildpack") + .DependsOn(Clean) + .Executes(() => + { + + Logger.Info(Stack); + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemFileVer) + .SetInformationalVersion(GitVersion.InformationalVersion) + .CombineWith(PublishCombinations, (c, p) => c + .SetFramework(p.Framework) + .SetRuntime(p.Runtime))); + }); + + Target Publish => _ => _ + .Description("Packages buildpack in Cloud Foundry expected format into /artifacts directory") + .DependsOn(Clean) + .Executes(() => + { + foreach (var publishCombination in PublishCombinations) + { + var framework = publishCombination.Framework; + var runtime = publishCombination.Runtime; + var packageZipName = GetPackageZipName(runtime); + var workDirectory = TemporaryDirectory / "pack"; + EnsureCleanDirectory(TemporaryDirectory); + var buildpackProject = Solution.GetProject(BuildpackProjectName); + if(buildpackProject == null) + throw new Exception($"Unable to find project called {BuildpackProjectName} in solution {Solution.Name}"); + var publishDirectory = buildpackProject.Directory / "bin" / Configuration / framework / runtime / "publish"; + var workBinDirectory = workDirectory / "bin"; + + + DotNetPublish(s => s + .SetProject(Solution) + .SetConfiguration(Configuration) + .SetFramework(framework) + .SetRuntime(runtime) + .EnableSelfContained() + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemFileVer) + .SetInformationalVersion(GitVersion.InformationalVersion) + ); + + var lifecycleBinaries = Solution.GetProjects("Lifecycle*") + .Select(x => x.Directory / "bin" / Configuration / framework / runtime / "publish") + .SelectMany(x => Directory.GetFiles(x).Where(path => LifecycleHooks.Any(hook => Path.GetFileName(path).StartsWith(hook)))); + + foreach (var lifecycleBinary in lifecycleBinaries) + { + CopyFileToDirectory(lifecycleBinary, workBinDirectory, FileExistsPolicy.OverwriteIfNewer); + } + + CopyDirectoryRecursively(publishDirectory, workBinDirectory, DirectoryExistsPolicy.Merge); + var tempZipFile = TemporaryDirectory / packageZipName; + + ZipFile.CreateFromDirectory(workDirectory, tempZipFile, CompressionLevel.NoCompression, false); + MakeFilesInZipUnixExecutable(tempZipFile); + CopyFileToDirectory(tempZipFile, ArtifactsDirectory, FileExistsPolicy.Overwrite); + Logger.Block(ArtifactsDirectory / packageZipName); + } + }); + + + Target Release => _ => _ + .Description("Creates a GitHub release (or amends existing) and uploads buildpack artifact") + .DependsOn(Publish) + .Requires(() => GitHubToken) + .Executes(async () => + { + foreach (var publishCombination in PublishCombinations) + { + var runtime = publishCombination.Runtime; + var packageZipName = GetPackageZipName(runtime); + if (!GitRepository.IsGitHubRepository()) + throw new Exception("Only supported when git repo remote is github"); + + var client = new GitHubClient(new ProductHeaderValue(BuildpackProjectName)) + { + Credentials = new Credentials(GitHubToken, AuthenticationType.Bearer) + }; + var gitIdParts = GitRepository.Identifier.Split("/"); + var owner = gitIdParts[0]; + var repoName = gitIdParts[1]; + + var releaseName = $"v{GitVersion.MajorMinorPatch}"; + Release release; + try + { + release = await client.Repository.Release.Get(owner, repoName, releaseName); + } + catch (NotFoundException) + { + var newRelease = new NewRelease(releaseName) + { + Name = releaseName, + Draft = false, + Prerelease = false + }; + release = await client.Repository.Release.Create(owner, repoName, newRelease); + } + + var existingAsset = release.Assets.FirstOrDefault(x => x.Name == packageZipName); + if (existingAsset != null) + { + await client.Repository.Release.DeleteAsset(owner, repoName, existingAsset.Id); + } + + var zipPackageLocation = ArtifactsDirectory / packageZipName; + var stream = File.OpenRead(zipPackageLocation); + var releaseAssetUpload = new ReleaseAssetUpload(packageZipName, "application/zip", stream, TimeSpan.FromHours(1)); + var releaseAsset = await client.Repository.Release.UploadAsset(release, releaseAssetUpload); + + Logger.Block(releaseAsset.BrowserDownloadUrl); + } + }); + + Target Detect => _ => _ + .Description("Invokes buildpack 'detect' lifecycle event") + .Requires(() => ApplicationDirectory) + .Executes(() => + { + try + { + DotNetRun(s => s + .SetProjectFile(Solution.GetProject("Lifecycle.Detect").Path) + .SetApplicationArguments(ApplicationDirectory) + .SetConfiguration(Configuration) + .SetFramework("netcoreapp3.1")); + Logger.Block("Detect returned 'true'"); + } + catch (ProcessException) + { + Logger.Block("Detect returned 'false'"); + } + }); + + Target Supply => _ => _ + .Description("Invokes buildpack 'supply' lifecycle event") + .Requires(() => ApplicationDirectory) + .Executes(() => + { + var home = (AbsolutePath)Path.GetTempPath() / Guid.NewGuid().ToString(); + var app = home / "app"; + var deps = home / "deps"; + var index = 0; + var cache = home / "cache"; + CopyDirectoryRecursively(ApplicationDirectory, app); + + DotNetRun(s => s + .SetProjectFile(Solution.GetProject("Lifecycle.Supply").Path) + .SetApplicationArguments($"{app} {cache} {app} {deps} {index}") + .SetConfiguration(Configuration) + .SetFramework("netcoreapp3.1")); + Logger.Block($"Buildpack applied. Droplet is available in {home}"); + + }); + + public void MakeFilesInZipUnixExecutable(AbsolutePath zipFile) + { + var tmpFileName = zipFile + ".tmp"; + using (var input = new ZipInputStream(File.Open(zipFile, FileMode.Open))) + using (var output = new ZipOutputStream(File.Open(tmpFileName, FileMode.Create))) + { + output.SetLevel(9); + ZipEntry entry; + + while ((entry = input.GetNextEntry()) != null) + { + var outEntry = new ZipEntry(entry.Name) {HostSystem = (int) HostSystemID.Unix}; + var entryAttributes = + ZipEntryAttributes.ReadOwner | + ZipEntryAttributes.ReadOther | + ZipEntryAttributes.ReadGroup | + ZipEntryAttributes.ExecuteOwner | + ZipEntryAttributes.ExecuteOther | + ZipEntryAttributes.ExecuteGroup; + entryAttributes = entryAttributes | (entry.IsDirectory ? ZipEntryAttributes.Directory : ZipEntryAttributes.Regular); + outEntry.ExternalFileAttributes = (int) (entryAttributes) << 16; // https://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute + output.PutNextEntry(outEntry); + input.CopyTo(output); + } + output.Finish(); + output.Flush(); + } + + DeleteFile(zipFile); + RenameFile(tmpFileName,zipFile, FileExistsPolicy.Overwrite); + } + + [Flags] + enum ZipEntryAttributes + { + ExecuteOther = 1, + WriteOther = 2, + ReadOther = 4, + + ExecuteGroup = 8, + WriteGroup = 16, + ReadGroup = 32, + + ExecuteOwner = 64, + WriteOwner = 128, + ReadOwner = 256, + + Sticky = 512, // S_ISVTX + SetGroupIdOnExecution = 1024, + SetUserIdOnExecution = 2048, + + //This is the file type constant of a block-oriented device file. + NamedPipe = 4096, + CharacterSpecial = 8192, + Directory = 16384, + Block = 24576, + Regular = 32768, + SymbolicLink = 40960, + Socket = 49152 + + } + class PublishTarget + { + public string Framework { get; set; } + public string Runtime { get; set; } + } +} diff --git a/build/_build.csproj b/build/_build.csproj new file mode 100644 index 0000000..55ab922 --- /dev/null +++ b/build/_build.csproj @@ -0,0 +1,37 @@ + + + + + Exe + net6.0 + false + False + CS0649;CS0169 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/_build.csproj.DotSettings b/build/_build.csproj.DotSettings new file mode 100644 index 0000000..9aac7d8 --- /dev/null +++ b/build/_build.csproj.DotSettings @@ -0,0 +1,24 @@ + + False + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True + True diff --git a/src/KerberosBuildpack/BuildpackBase.cs b/src/KerberosBuildpack/BuildpackBase.cs new file mode 100644 index 0000000..12ff60a --- /dev/null +++ b/src/KerberosBuildpack/BuildpackBase.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace KerberosBuildpack +{ + public abstract class BuildpackBase + { + /// + /// Dictionary of environmental variables to be set at runtime before the app starts + /// + protected Dictionary EnvironmentalVariables { get; } = new Dictionary(); + protected bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + + /// + /// Determines if the buildpack is compatible and should be applied to the application being staged. + /// + /// Directory path to the application + /// True if buildpack should be applied, otherwise false + public abstract bool Detect(string buildPath); + public abstract void Supply(string buildPath, string cachePath, string depsPath, int index); + public abstract void Finalize(string buildPath, string cachePath, string depsPath, int index); + public abstract void Release(string buildPath); + + /// + /// Code that will execute during the run stage before the app is started + /// + public virtual void PreStartup(string buildPath, string depsPath, int index) + { + } + + /// + /// Logic to apply when buildpack is ran. + /// Note that for this will correspond to "bin/supply" lifecycle event, while for it will be invoked on "bin/finalize" + /// + /// Directory path to the application + /// Location the buildpack can use to store assets during the build process + /// Directory where dependencies provided by all buildpacks are installed. New dependencies introduced by current buildpack should be stored inside subfolder named with index argument ({depsPath}/{index}) + /// Number that represents the ordinal position of the buildpack + protected abstract void Apply(string buildPath, string cachePath, string depsPath, int index); + + + public void PreStartup(int index) + { + var appHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + appHome = Path.Combine(appHome, "app"); + PreStartup(appHome, Environment.GetEnvironmentVariable("DEPS_DIR"), index); + var profiled = Path.Combine(appHome, ".profile.d"); + InstallStartupEnvVars(profiled, index, true); + } + protected void DoApply(string buildPath, string cachePath, string depsPath, int index) + { + Apply(buildPath, cachePath, depsPath, index); + + var isPreStartOverriden = GetType().GetMethod(nameof(PreStartup), BindingFlags.Instance | BindingFlags.Public, null, new[] {typeof(string),typeof(string),typeof(int) }, null )?.DeclaringType != typeof(BuildpackBase); + var buildpackDepsDir = Path.Combine(depsPath, index.ToString()); + Directory.CreateDirectory(buildpackDepsDir); + var profiled = Path.Combine(buildPath, ".profile.d"); + Directory.CreateDirectory(profiled); + + if (isPreStartOverriden) + { + // copy buildpack to deps dir so we can invoke it as part of startup + foreach(var file in Directory.EnumerateFiles(Path.GetDirectoryName(GetType().Assembly.Location))) + { + File.Copy(file, Path.Combine(buildpackDepsDir, Path.GetFileName(file)), true); + } + + var extension = !IsLinux ? ".exe" : string.Empty; + var prestartCommand = $"{GetType().Assembly.GetName().Name}{extension} PreStartup"; + // write startup shell script to call buildpack prestart lifecycle event in deps dir + var startupScriptName = $"{index:00}_{nameof(KerberosBuildpack)}_startup"; + if (IsLinux) + { + File.WriteAllText(Path.Combine(profiled,$"{startupScriptName}.sh"), $"#!/bin/bash\n$DEPS_DIR/{index}/{prestartCommand} {index}"); + } + else + { + File.WriteAllText(Path.Combine(profiled,$"{startupScriptName}.bat"),$@"%DEPS_DIR%\{index}\{prestartCommand} {index}"); + } + InstallStartupEnvVars(profiled, index, false); + GetEnvScriptFile(profiled, index, true); // causes empty env file to be created so it can (potentially) be populated with vars during onstart hook + } + + } + private string GetEnvScriptFile(string profiled, int index, bool isPreStart) + { + var prefix = isPreStart ? "z" : string.Empty; + var suffix = IsLinux ? ".sh" : ".bat"; + var envScriptName = Path.Combine(profiled, $"{prefix}{index:00}_{nameof(KerberosBuildpack)}_env{suffix}"); + // ensure it's initialized + if(!File.Exists(envScriptName)) + File.WriteAllText(envScriptName, string.Empty); + return envScriptName; + } + protected void InstallStartupEnvVars(string profiled, int index, bool isPreStart) + { + var envScriptName = GetEnvScriptFile(profiled, index, isPreStart); + + if (EnvironmentalVariables.Any()) + { + if (IsLinux) + { + var envVars = EnvironmentalVariables.Aggregate(new StringBuilder(), (sb,x) => sb.Append($"export {x.Key}={Escape(x.Value)}\n")); + File.WriteAllText(envScriptName, $"#!/bin/bash\n{envVars}"); + } + else + { + var envVars = EnvironmentalVariables.Aggregate(new StringBuilder(), (sb,x) => sb.Append($"SET {x.Key}={x.Value}\r\n")); + File.WriteAllText(envScriptName,envVars.ToString()); + } + } + + } + + private static string Escape(string value) => $"\"{value.Replace("\"", "\\\"")}\""; + } +} \ No newline at end of file diff --git a/src/KerberosBuildpack/Commands.cs b/src/KerberosBuildpack/Commands.cs new file mode 100644 index 0000000..54e51eb --- /dev/null +++ b/src/KerberosBuildpack/Commands.cs @@ -0,0 +1,40 @@ +using CommandDotNet; + +namespace KerberosBuildpack +{ + public class Commands + { + private readonly KerberosBuildpack _buildpack = new KerberosBuildpack(); + + public int Detect([Operand(Description = "Directory path to the application")]string buildPath) + { + return _buildpack.Detect(buildPath) ? 0 : 1; + } + + public void Supply([Operand(Description = "Directory path to the application")]string buildPath, + [Operand(Description = "Location the buildpack can use to store assets during the build process")] string cachePath, + [Operand(Description = "Directory where dependencies provided by all buildpacks are installed. New dependencies introduced by current buildpack should be stored inside subfolder named with index argument ({depsPath}/{index})")] string depsPath, + [Operand(Description = "Number that represents the ordinal position of the buildpack")] int index) + { + _buildpack.Supply(buildPath, cachePath, depsPath, index); + } + + public void Finalize([Operand(Description = "Directory path to the application")]string buildPath, + [Operand(Description = "Location the buildpack can use to store assets during the build process")] string cachePath, + [Operand(Description = "Directory where dependencies provided by all buildpacks are installed. New dependencies introduced by current buildpack should be stored inside subfolder named with index argument ({depsPath}/{index})")] string depsPath, + [Operand(Description = "Number that represents the ordinal position of the buildpack")] int index) + { + _buildpack.Finalize(buildPath,cachePath, depsPath, index); + } + + public void Release([Operand(Description = "Directory path to the application")]string buildPath) + { + _buildpack.Release(buildPath); + } + + public void PreStartup([Operand(Description = "Number that represents the ordinal position of the buildpack")]int index) + { + _buildpack.PreStartup(index); + } + } +} \ No newline at end of file diff --git a/src/KerberosBuildpack/FinalBuildpack.cs b/src/KerberosBuildpack/FinalBuildpack.cs new file mode 100644 index 0000000..445c907 --- /dev/null +++ b/src/KerberosBuildpack/FinalBuildpack.cs @@ -0,0 +1,31 @@ +using System; + +namespace KerberosBuildpack +{ + public abstract class FinalBuildpack : BuildpackBase + { + public sealed override void Supply(string buildPath, string cachePath, string depsPath, int index) + { + // do nothing, we always apply in finalize + } + + public sealed override void Finalize(string buildPath, string cachePath, string depsPath, int index) + { + DoApply(buildPath, cachePath, depsPath, index); + } + + public sealed override void Release(string buildPath) + { + Console.WriteLine("default_process_types:"); + Console.WriteLine($" web: {GetStartupCommand(buildPath)}"); + } + + /// + /// Determines the startup command for the app + /// + /// Directory path to the application + /// Startup command executed by Cloud Foundry to launch the application + public abstract string GetStartupCommand(string buildPath); + + } +} \ No newline at end of file diff --git a/src/KerberosBuildpack/KerberosBuildpack.cs b/src/KerberosBuildpack/KerberosBuildpack.cs new file mode 100644 index 0000000..b9d71b5 --- /dev/null +++ b/src/KerberosBuildpack/KerberosBuildpack.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using Kerberos.NET.Configuration; +using KerberosCommon; + +namespace KerberosBuildpack +{ + public class KerberosBuildpack : SupplyBuildpack + { + + protected override void Apply(string buildPath, string cachePath, string depsPath, int index) + { + var myDependenciesDirectory = Path.Combine(depsPath, index.ToString()); // store any runtime dependencies not belonging to the app in this directory + + if (!Util.TryGetCredentials(out var credentials)) + { + return; + } + var realm = credentials.Domain.ToUpper(); + var kdcStr = Environment.GetEnvironmentVariable("KRB5_KDC"); + var kdcs = kdcStr != null ? kdcStr.Split(";") : new []{ credentials.Domain }; + + + EnvironmentalVariables["KRB5_CONFIG"] = "/home/vcap/app/.krb5/krb5.conf"; + EnvironmentalVariables["KRB5CCNAME"] = "/home/vcap/app/.krb5/krb5cc"; + var krb5Dir = Path.Combine(buildPath, ".krb5"); + var krb5Path = Path.Combine(krb5Dir, "krb5.conf"); + Directory.CreateDirectory(krb5Dir); + Krb5Config config; + if (!File.Exists(krb5Path)) // allow user to provide their own krb5 with the app + { + config = Krb5Config.Default(); + config.Defaults.DefaultRealm = realm; + foreach (var kdc in kdcs) + { + config.Realms[realm].Kdc.Add(kdc); + } + + File.WriteAllText(krb5Path, config.Serialize()); + } + + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = assembly.GetManifestResourceNames().Single(x => x.EndsWith("launch.yaml")); + + using var stream = assembly.GetManifestResourceStream(resourceName); + using var reader = new StreamReader(stream); + string template = reader.ReadToEnd(); + + var launchYaml = template.Replace("@bpIndex", index.ToString()); + File.WriteAllText(Path.Combine(myDependenciesDirectory, "launch.yaml"), launchYaml); + } + public override void PreStartup(string buildPath, string depsPath, int index) + { + KerberosTicketRefresher.Program.Main(new[]{"--Krb_RunOnce=true"}).Wait(); + EnvironmentalVariables["MY_SETTING"] = "value"; // can set env vars before app starts running + } + + + } +} diff --git a/src/KerberosBuildpack/KerberosBuildpack.csproj b/src/KerberosBuildpack/KerberosBuildpack.csproj new file mode 100644 index 0000000..863fb8b --- /dev/null +++ b/src/KerberosBuildpack/KerberosBuildpack.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + linux-x64 + KerberosBuildpack + buildpack + Exe + + + + + + + + + + + + + + + + + diff --git a/src/KerberosBuildpack/Program.cs b/src/KerberosBuildpack/Program.cs new file mode 100644 index 0000000..d09e080 --- /dev/null +++ b/src/KerberosBuildpack/Program.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; +using System.Linq; +using CommandDotNet; +using CommandDotNet.Directives; +using CommandDotNet.Execution; + +namespace KerberosBuildpack +{ + public class Program + { + public static int Main(string[] args) + { + var runner = new AppRunner() + .Configure(cfg => cfg.UseMiddleware(async (context, next) => + { + // make all parameters mandatory + var invocation = context.InvocationPipeline.TargetCommand.Invocation; + var missingParameters = invocation.Parameters + .Select(x => x.Name) + .Zip(invocation.ParameterValues, (s, o) => new {Name = s, Value = o}) + .Where(x => x.Value == null) + .ToList(); + if (missingParameters.Any()) + { + var console = context.Console; + var help = context.AppConfig.HelpProvider.GetHelpText(context.InvocationPipeline.TargetCommand.Command); + console.Out.WriteLine(help); + return 1; + } + + return await next(context); + + }, MiddlewareStages.PostBindValuesPreInvoke )); + runner.AppSettings.IgnoreUnexpectedOperands = true; + runner.AppSettings.DefaultArgumentMode = ArgumentMode.Operand; + return runner.Run(args); + } + } +} \ No newline at end of file diff --git a/src/KerberosBuildpack/SidecarProcessConfig.cs b/src/KerberosBuildpack/SidecarProcessConfig.cs new file mode 100644 index 0000000..6575860 --- /dev/null +++ b/src/KerberosBuildpack/SidecarProcessConfig.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace KerberosBuildpack; + +public class SidecarProcessConfig +{ + public List Processes { get; set; } + + public class SidecarProcess + { + public string Type { get; set; } + public string Command { get; set; } + public string Limits { get; set; } + } +} \ No newline at end of file diff --git a/src/KerberosBuildpack/SupplyBuildpack.cs b/src/KerberosBuildpack/SupplyBuildpack.cs new file mode 100644 index 0000000..5d3f1d7 --- /dev/null +++ b/src/KerberosBuildpack/SupplyBuildpack.cs @@ -0,0 +1,31 @@ +namespace KerberosBuildpack +{ + public abstract class SupplyBuildpack : BuildpackBase + { + public sealed override void Supply(string buildPath, string cachePath, string depsPath, int index) + { + DoApply(buildPath, cachePath, depsPath, index); + } + + + // supply buildpacks may get this lifecycle event, but since only one buildpack will be selected if detection is used, it must be final + // therefore supply buildpacks always must reply with false + public sealed override bool Detect(string buildPath) => false; + + /// + /// Only executed on final buildpack, so not applicable to supply buildpacks + /// + public sealed override void Finalize(string buildPath, string cachePath, string depsPath, int index) + { + + } + + /// + /// Only executed on final buildpack, so not applicable to supply buildpacks + /// + public override void Release(string buildPath) + { + + } + } +} \ No newline at end of file diff --git a/src/KerberosBuildpack/launch.yaml b/src/KerberosBuildpack/launch.yaml new file mode 100644 index 0000000..9699184 --- /dev/null +++ b/src/KerberosBuildpack/launch.yaml @@ -0,0 +1,9 @@ +--- +processes: + - type: "krb-tgt-refresher" + command: "$DEPS_DIR/@bpIndex/buildpack PreStartup @bpIndex" + limits: + memory: 256 + platforms: + cloudfoundry: + sidecar_for: ["web"] \ No newline at end of file diff --git a/src/KerberosCommon/KerberosCommon.csproj b/src/KerberosCommon/KerberosCommon.csproj new file mode 100644 index 0000000..132c02c --- /dev/null +++ b/src/KerberosCommon/KerberosCommon.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/src/KerberosCommon/Util.cs b/src/KerberosCommon/Util.cs new file mode 100644 index 0000000..7a1c7fd --- /dev/null +++ b/src/KerberosCommon/Util.cs @@ -0,0 +1,56 @@ +using System.Net; + +namespace KerberosCommon; + +public class Util +{ + public static bool TryGetCredentials(out NetworkCredential credential) + { + if (!TryGetCredentials(out credential, out var errors)) + { + + foreach (var error in errors) + { + Console.Error.WriteLine(error); + return false; + } + } + + return true; + } + public static bool TryGetCredentials(out NetworkCredential credential, out List errors) + { + credential = default!; + var upn = Environment.GetEnvironmentVariable("KRB_SERVICE_ACCOUNT"); + errors = new List(); + string[] principalParts = new string[1]; + + if (upn == null) + { + errors.Add("Required KRB_SERVICE_ACCOUNT environmental variable is not set. Must be set to service account under which service will run provided in account@domain.com format"); + } + else + { + principalParts = upn.Split("@"); + if (principalParts.Length != 2) + { + errors.Add("KRB_SERVICE_ACCOUNT must be in account@domain.com format"); + } + } + + var password = Environment.GetEnvironmentVariable("KRB_PASSWORD"); + if (password == null) + { + errors.Add("Required KRB_PASSWORD environmental variable is not set"); + } + + if (errors.Any()) + { + return false; + } + + + credential = new NetworkCredential(principalParts[0], password, principalParts[1]); + return true; + } +} \ No newline at end of file diff --git a/src/KerberosCommon/ValidationException.cs b/src/KerberosCommon/ValidationException.cs new file mode 100644 index 0000000..2d18c0b --- /dev/null +++ b/src/KerberosCommon/ValidationException.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; + +namespace KerberosCommon; + +public class ValidationException : Exception +{ + public ValidationException() + { + } + + protected ValidationException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public ValidationException(string? message) : base(message) + { + } + + public ValidationException(string? message, Exception? innerException) : base(message, innerException) + { + } + + public List Errors { get; set; } = new(); + public override string Message => string.Join("\n", Errors); +} \ No newline at end of file diff --git a/src/KerberosTicketRefresher/KerberosOptions.cs b/src/KerberosTicketRefresher/KerberosOptions.cs new file mode 100644 index 0000000..0ed0199 --- /dev/null +++ b/src/KerberosTicketRefresher/KerberosOptions.cs @@ -0,0 +1,14 @@ +using Kerberos.NET.Credentials; + +namespace KerberosTicketRefresher; + +public class KerberosOptions +{ + public string Kerb5ConfigFile { get; set; } = null!; + public string ServiceAccount { get; set; } = null!; + public string Password { get; set; } = null!; + public string CacheFile { get; set; } = null!; + + // public KerberosPasswordCredential KerberosCredentials { get; set; } + public bool RunOnce { get; set; } +} \ No newline at end of file diff --git a/src/KerberosTicketRefresher/KerberosTicketRefresher.csproj b/src/KerberosTicketRefresher/KerberosTicketRefresher.csproj new file mode 100644 index 0000000..b98acb0 --- /dev/null +++ b/src/KerberosTicketRefresher/KerberosTicketRefresher.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + linux-x64 + + enable + enable + exe + + + + + + + + + + + diff --git a/src/KerberosTicketRefresher/Program.cs b/src/KerberosTicketRefresher/Program.cs new file mode 100644 index 0000000..066244c --- /dev/null +++ b/src/KerberosTicketRefresher/Program.cs @@ -0,0 +1,63 @@ +using System.IO; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Kerberos.NET.Client; +using Kerberos.NET.Configuration; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KerberosTicketRefresher; + +public class Program +{ + public static async Task Main(string[] args) + { + IHost host = Host.CreateDefaultBuilder(args) + .ConfigureServices((context, services) => + { + services.AddOptions() + .Configure(c => + { + var config = context.Configuration; + c.CacheFile = config.GetValue("KRB5CCNAME"); + c.Kerb5ConfigFile = config.GetValue("KRB5_CONFIG"); + c.ServiceAccount = config.GetValue("KRB_SERVICE_ACCOUNT"); + c.Password = config.GetValue("KRB_PASSWORD"); + c.RunOnce = config.GetValue("KRB_RunOnce"); + }) + .Validate(c => c.Kerb5ConfigFile != "null", "Required KRB5_CONFIG environmental variable is not set") + .Validate(c => File.Exists(c.Kerb5ConfigFile), "KRB5_CONFIG points to file that doesn't exist") + .Validate(c => c.CacheFile != "null", "Required KRB5CCNAME environmental variable is not set") + .Validate(c => c.ServiceAccount != null && Regex.IsMatch(c.ServiceAccount, "^.+?@.+$"), "KRB_SERVICE_ACCOUNT must be set in user@domain.com format") + .Validate(c => c.Password != null, "Required KRB_PASSWORD must be set"); + + services.AddSingleton(svc => + { + var options = svc.GetRequiredService>().Value; + var config = Krb5Config.Parse(File.ReadAllText(options.Kerb5ConfigFile)); + config.Defaults.DefaultCCacheName = options.CacheFile; + return config; + }); + + services.AddSingleton(svc => + { + var config = svc.GetRequiredService(); + var loggerFactory = svc.GetRequiredService(); + var options = svc.GetRequiredService>().Value; + + var client = new KerberosClient(config, loggerFactory); + client.CacheInMemory = false; + client.Cache = new Krb5TicketCache(options.CacheFile); + client.RenewTickets = true; + return client; + }); + services.AddHostedService(); + }) + .Build(); + + await host.RunAsync(); + } +} \ No newline at end of file diff --git a/src/KerberosTicketRefresher/Properties/launchSettings.json b/src/KerberosTicketRefresher/Properties/launchSettings.json new file mode 100644 index 0000000..28c6412 --- /dev/null +++ b/src/KerberosTicketRefresher/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "KerberosTicketRefresher": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/KerberosTicketRefresher/Worker.cs b/src/KerberosTicketRefresher/Worker.cs new file mode 100644 index 0000000..d148b4c --- /dev/null +++ b/src/KerberosTicketRefresher/Worker.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Kerberos.NET.Client; +using Kerberos.NET.Configuration; +using Kerberos.NET.Credentials; +using KerberosCommon; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace KerberosTicketRefresher; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + private readonly KerberosClient _client; + private readonly IHostApplicationLifetime _lifetime; + private readonly KerberosOptions _options; + + public Worker(ILogger logger, KerberosClient client, IOptions options, IHostApplicationLifetime lifetime) + { + _logger = logger; + _client = client; + _lifetime = lifetime; + _options = options.Value; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // KerberosClient has it's own background task to renew TGT, we just gotta keep the app alive after getting initial one + var credentials = new KerberosPasswordCredential(_options.ServiceAccount, _options.Password); + await _client.Authenticate(credentials); + _logger.LogInformation("Initial TGT acquired"); + if (_options.RunOnce) + { + _lifetime.StopApplication(); + } + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(1000, stoppingToken); + } + } +} diff --git a/src/KerberosTicketRefresher/appsettings.Development.json b/src/KerberosTicketRefresher/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/src/KerberosTicketRefresher/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/KerberosTicketRefresher/appsettings.json b/src/KerberosTicketRefresher/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/src/KerberosTicketRefresher/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Lifecycle.Detect/Lifecycle.Detect.csproj b/src/Lifecycle.Detect/Lifecycle.Detect.csproj new file mode 100644 index 0000000..a485e8a --- /dev/null +++ b/src/Lifecycle.Detect/Lifecycle.Detect.csproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0 + detect + + + + + + + diff --git a/src/Lifecycle.Detect/Program.cs b/src/Lifecycle.Detect/Program.cs new file mode 100644 index 0000000..2e4939a --- /dev/null +++ b/src/Lifecycle.Detect/Program.cs @@ -0,0 +1,4 @@ +using System.Linq; + +var argsWithCommand = new[] {"Detect"}.Concat(args).ToArray(); +return KerberosBuildpack.Program.Main(argsWithCommand); \ No newline at end of file diff --git a/src/Lifecycle.Finalize/Lifecycle.Finalize.csproj b/src/Lifecycle.Finalize/Lifecycle.Finalize.csproj new file mode 100644 index 0000000..e00cfa8 --- /dev/null +++ b/src/Lifecycle.Finalize/Lifecycle.Finalize.csproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0 + finalize + + + + + + + diff --git a/src/Lifecycle.Finalize/Program.cs b/src/Lifecycle.Finalize/Program.cs new file mode 100644 index 0000000..c68024d --- /dev/null +++ b/src/Lifecycle.Finalize/Program.cs @@ -0,0 +1,4 @@ +using System.Linq; + +var argsWithCommand = new[] {"Finalize"}.Concat(args).ToArray(); +return KerberosBuildpack.Program.Main(argsWithCommand); \ No newline at end of file diff --git a/src/Lifecycle.Release/Lifecycle.Release.csproj b/src/Lifecycle.Release/Lifecycle.Release.csproj new file mode 100644 index 0000000..0948504 --- /dev/null +++ b/src/Lifecycle.Release/Lifecycle.Release.csproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0 + release + + + + + + + diff --git a/src/Lifecycle.Release/Program.cs b/src/Lifecycle.Release/Program.cs new file mode 100644 index 0000000..c784266 --- /dev/null +++ b/src/Lifecycle.Release/Program.cs @@ -0,0 +1,4 @@ +using System.Linq; + +var argsWithCommand = new[] {"Release"}.Concat(args).ToArray(); +return KerberosBuildpack.Program.Main(argsWithCommand); \ No newline at end of file diff --git a/src/Lifecycle.Supply/Lifecycle.Supply.csproj b/src/Lifecycle.Supply/Lifecycle.Supply.csproj new file mode 100644 index 0000000..43da33c --- /dev/null +++ b/src/Lifecycle.Supply/Lifecycle.Supply.csproj @@ -0,0 +1,13 @@ + + + + Exe + net6.0 + supply + + + + + + + diff --git a/src/Lifecycle.Supply/Program.cs b/src/Lifecycle.Supply/Program.cs new file mode 100644 index 0000000..c00b1fa --- /dev/null +++ b/src/Lifecycle.Supply/Program.cs @@ -0,0 +1,4 @@ +using System.Linq; + +var argsWithCommand = new[] {"Supply"}.Concat(args).ToArray(); +return KerberosBuildpack.Program.Main(argsWithCommand); \ No newline at end of file