Create Pulumi-provisioned web server

This commit is contained in:
Smaug123
2022-05-01 14:13:21 +01:00
commit 61611ccc2c
49 changed files with 3667 additions and 0 deletions

12
.config/dotnet-tools.json Normal file
View File

@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fantomas": {
"version": "5.2.0-alpha-010",
"commands": [
"fantomas"
]
}
}
}

41
.editorconfig Normal file
View File

@@ -0,0 +1,41 @@
root=true
[*]
charset=utf-8
end_of_line=crlf
trim_trailing_whitespace=true
insert_final_newline=true
indent_style=space
indent_size=4
# ReSharper properties
resharper_xml_indent_size=2
resharper_xml_max_line_length=100
resharper_xml_tab_width=2
[*.{csproj,fsproj,sqlproj,targets,props,ts,tsx,css,json}]
indent_style=space
indent_size=2
[*.{fs,fsi}]
fsharp_bar_before_discriminated_union_declaration=true
fsharp_space_before_uppercase_invocation=true
fsharp_space_before_class_constructor=true
fsharp_space_before_member=true
fsharp_space_before_colon=true
fsharp_space_before_semicolon=true
fsharp_multiline_block_brackets_on_same_column=true
fsharp_newline_between_type_definition_and_members=true
fsharp_align_function_signature_to_indentation=true
fsharp_alternative_long_member_definitions=true
fsharp_multi_line_lambda_closing_newline=true
fsharp_experimental_keep_indent_in_branch=true
fsharp_max_value_binding_width=80
fsharp_max_record_width=0
max_line_length=120
end_of_line=lf
[*.{appxmanifest,build,dtd,nuspec,xaml,xamlx,xoml,xsd}]
indent_style=space
indent_size=2
tab_width=2

3
.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
* eol=auto
*.sh eol=lf
*.nix eol=lf

94
.github/workflows/dotnetcore.yml vendored Normal file
View File

@@ -0,0 +1,94 @@
name: .NET
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
jobs:
build:
strategy:
matrix:
config:
- Release
- Debug
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration ${{matrix.config}}
- name: Test
run: dotnet test --no-build --verbosity normal --configuration ${{matrix.config}}
check-dotnet-format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Nix
uses: cachix/install-nix-action@v17
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Run Fantomas
run: nix run .#fantomas -- -r --check .
check-nix-format:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Nix
uses: cachix/install-nix-action@v17
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Run Alejandra
run: nix develop .#ci --command alejandra --check .
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
name: Checkout
- name: Install Nix
uses: cachix/install-nix-action@v17
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Run ShellCheck
run: nix develop .#ci --command bash -c "find . -type f -name '*.sh' | xargs shellcheck"
linkcheck:
name: Check links
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Install Nix
uses: cachix/install-nix-action@v17
with:
extra_nix_config: |
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
- name: Run link checker
run: nix develop .#ci --command markdown-link-check README.md
all-required-checks-complete:
needs: [check-dotnet-format, check-nix-format, build, shellcheck, linkcheck]
runs-on: ubuntu-latest
steps:
- run: echo "All required checks complete."

361
.gitignore vendored Normal file
View File

@@ -0,0 +1,361 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.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
# Visual Studio Trace Files
*.e2e
# 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
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# 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
# Note: 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
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable 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
*.appx
*.appxbundle
*.appxupload
# 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
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# 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
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# 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/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
.idea/
/result
.DS_Store
/.profile
/.profile-*-link

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Patrick Stevens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
Pulumi.sln Normal file
View File

@@ -0,0 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PulumiWebServer", "PulumiWebServer\PulumiWebServer.fsproj", "{289B2402-80C5-47EB-896F-BEF9A751DE61}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "PulumiWebServer.Test", "PulumiWebServer.Test\PulumiWebServer.Test.fsproj", "{4F472FBB-36FB-4073-A7B1-FC102D2D209E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{289B2402-80C5-47EB-896F-BEF9A751DE61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{289B2402-80C5-47EB-896F-BEF9A751DE61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{289B2402-80C5-47EB-896F-BEF9A751DE61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{289B2402-80C5-47EB-896F-BEF9A751DE61}.Release|Any CPU.Build.0 = Release|Any CPU
{4F472FBB-36FB-4073-A7B1-FC102D2D209E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F472FBB-36FB-4073-A7B1-FC102D2D209E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F472FBB-36FB-4073-A7B1-FC102D2D209E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F472FBB-36FB-4073-A7B1-FC102D2D209E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

2
Pulumi.sln.DotSettings Normal file
View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Radicale/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Include="Utils.fs" />
<Compile Include="TestConfiguration.fs" />
<Compile Include="TestJsonSchema.fs" />
<EmbeddedResource Include="exampleconfig.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsCheck" Version="2.16.5" />
<PackageReference Include="FsUnit" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="NJsonSchema" Version="10.8.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PulumiWebServer\PulumiWebServer.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,103 @@
namespace PulumiWebServer.Test
open System
open System.IO
open NUnit.Framework
open FsCheck
open FsUnitTyped
open PulumiWebServer
[<TestFixture>]
module TestConfiguration =
let fileInfoGenerator =
gen {
let! fileName = Gen.choose (65, 90) |> Gen.map char |> Gen.arrayOf
return FileInfo (Path.Combine ("/tmp", String fileName))
}
let bashStringGenerator =
gen {
let! s = Arb.generate<string>
return BashString.make s
}
let radicaleConfigGen =
gen {
let! password = Arb.generate<string>
let! username = Arb.generate<string>
let! optionValue = Arb.generate<bool>
if optionValue then
let! (NonNull s) = Arb.generate<NonNull<string>>
return
{
RadicaleConfig.User = username
RadicaleConfig.Password = password
RadicaleConfig.GitEmail = Some s
}
else
return
{
RadicaleConfig.User = username
RadicaleConfig.Password = password
RadicaleConfig.GitEmail = None
}
}
type MyGenerators =
static member FileInfo () =
{ new Arbitrary<FileInfo>() with
override x.Generator = fileInfoGenerator
override x.Shrinker t = Seq.empty
}
static member BashString () =
{ new Arbitrary<BashString>() with
override x.Generator = bashStringGenerator
override x.Shrinker t = Seq.empty
}
static member RadicaleConfig () =
{ new Arbitrary<RadicaleConfig>() with
override x.Generator = radicaleConfigGen
override x.Shrinker t = Seq.empty
}
[<Test>]
let ``Serialisation round-trip`` () =
Arb.register<MyGenerators> () |> ignore
let property (c : Configuration) : bool =
let serialised = SerialisedConfig.Make c
let roundTripped = SerialisedConfig.Deserialise serialised
c = roundTripped
property |> Check.QuickThrowOnFailure
[<Test>]
let ``Specific example`` () =
let config =
{
Name = ""
PrivateKey = PrivateKey (FileInfo "/tmp")
PublicKeyOverride = None
AcmeEmail = EmailAddress ""
Domain = DomainName ""
Cnames = Map.empty
Subdomains = Set.empty
RemoteUsername = Username ""
GiteaConfig = None
RadicaleConfig =
Some
{
User = ""
Password = ""
GitEmail = None
}
}
let serialised = SerialisedConfig.Make config
let roundTripped = SerialisedConfig.Deserialise serialised
config |> shouldEqual roundTripped

View File

@@ -0,0 +1,65 @@
namespace PulumiWebServer.Test
open System.IO
open System.Reflection
open PulumiWebServer
open NJsonSchema.Generation
open NJsonSchema.Validation
open NUnit.Framework
open FsUnitTyped
open NJsonSchema
open Newtonsoft.Json
open Newtonsoft.Json.Serialization
[<TestFixture>]
module TestSchema =
[<Test>]
let ``Example conforms to schema`` () =
let executing = Assembly.GetExecutingAssembly().Location |> FileInfo
let schemaFile =
Utils.findFileAbove "PulumiWebServer/config.schema.json" executing.Directory
let schema = JsonSchema.FromJsonAsync(File.ReadAllText schemaFile.FullName).Result
let json =
Utils.getEmbeddedResource typeof<Utils.Dummy>.Assembly "exampleconfig.json"
let validator = JsonSchemaValidator ()
let errors = validator.Validate (json, schema)
errors |> shouldBeEmpty
[<Test>]
let ``Example can be loaded`` () =
let config =
Utils.getEmbeddedResource typeof<Utils.Dummy>.Assembly "exampleconfig.json"
use stream = new MemoryStream ()
do
let writer = new StreamWriter (stream)
writer.WriteLine config
writer.Flush ()
stream.Seek (0L, SeekOrigin.Begin) |> ignore
Configuration.get stream |> ignore
[<Test>]
[<Explicit "Run this to regenerate the schema file">]
let ``Update schema file`` () =
let schemaFile =
Assembly.GetExecutingAssembly().Location
|> FileInfo
|> fun fi -> fi.Directory
|> Utils.findFileAbove "PulumiWebServer/config.schema.json"
let settings = JsonSchemaGeneratorSettings ()
settings.SerializerSettings <-
JsonSerializerSettings (ContractResolver = CamelCasePropertyNamesContractResolver ())
let schema = JsonSchema.FromType (typeof<SerialisedConfig>, settings)
File.WriteAllText (schemaFile.FullName, schema.ToJson ())

View File

@@ -0,0 +1,21 @@
namespace PulumiWebServer.Test
open System.IO
[<RequireQualifiedAccess>]
module Utils =
type Dummy =
class
end
let rec findFileAbove (fileName : string) (di : DirectoryInfo) =
if isNull di then
failwith "hit the root without finding anything"
let candidate = Path.Combine (di.FullName, fileName) |> FileInfo
if candidate.Exists then
candidate
else
findFileAbove fileName di.Parent

View File

@@ -0,0 +1,20 @@
{
"name": "server",
"privateKey": "/path/to/.ssh/id_ed25519",
"acmeEmail": "my_acme_email@example.com",
"domain": "staging.example.com",
"remoteUsername": "my-username",
"giteaConfig": {
"serverPassword": "password-for-gitea-linux-user",
"adminPassword": "gitea-admin-user-app-password",
"adminUsername": "gitea-admin-username",
"adminEmailAddress": "gitea_email@example.com"
},
"radicaleConfig": {
"user": "app-username",
"password": "app-password",
"gitEmail": "radicale_email@example.com"
},
"cnames": {"www": "root"},
"subdomains": ["gitea", "calendar"]
}

View File

@@ -0,0 +1,26 @@
namespace PulumiWebServer
type BashString =
private
{
Original : string
Safe : string
}
override this.ToString () = this.Safe
[<RequireQualifiedAccess>]
module BashString =
let make (s : string) =
{
Original = s
Safe =
// This is actually of course not safe, but it's
// close enough.
if System.Object.ReferenceEquals (s, null) then
null
else
s.Replace ("'", "'\"'\"'") |> sprintf "'%s'"
}
let unsafeOriginal (s : BashString) = s.Original

View File

@@ -0,0 +1,11 @@
namespace PulumiWebServer
type BashString
[<RequireQualifiedAccess>]
module BashString =
val make : string -> BashString
/// Get the original string that was used to make this BashString.
/// This is not safe to interpolate into a Bash script.
val unsafeOriginal : BashString -> string

View File

@@ -0,0 +1,122 @@
namespace PulumiWebServer
open Nager.PublicSuffix
open Pulumi
open Pulumi.Cloudflare
[<RequireQualifiedAccess>]
type ARecord =
{
IPv4 : Record option
IPv6 : Record option
}
type Cname =
{
Source : string
Target : string
Record : Record
}
type DnsRecord =
| Cname of Cname
| ARecord of ARecord
[<RequireQualifiedAccess>]
module Cloudflare =
let getZone (DomainName domain) : Output<ZoneId> =
let args = GetZoneInvokeArgs ()
args.Name <- domain
output {
let! zone = GetZone.Invoke args
return ZoneId zone.ZoneId
}
let makeARecord (zone : string) (name : string) (ipAddress : Address) =
let v6 =
match ipAddress.IPv6 with
| None -> None
| Some ipv6Addr ->
let args = RecordArgs ()
args.ZoneId <- Input.lift zone
args.Name <- Input.lift name
args.Ttl <- Input.lift 60
args.Type <- Input.lift "AAAA"
args.Value <- Input.lift ipv6Addr
Record ($"{name}-ipv6", args) |> Some
let v4 =
match ipAddress.IPv4 with
| None -> None
| Some ipv4Addr ->
let args = RecordArgs ()
args.ZoneId <- Input.lift zone
args.Name <- Input.lift name
args.Ttl <- Input.lift 60
args.Type <- Input.lift "A"
args.Value <- Input.lift ipv4Addr
Record ($"{name}-ipv4", args) |> Some
{
ARecord.IPv4 = v4
ARecord.IPv6 = v6
}
let addDns
(domain : DomainName)
(cnames : Map<WellKnownCname, WellKnownCnameTarget>)
(subdomains : Set<WellKnownSubdomain>)
(ZoneId zone)
(ipAddress : Address)
: Map<string, DnsRecord>
=
let globalSubdomain =
let (DomainName domain) = domain
let parser = DomainParser (WebTldRuleProvider ())
let info = parser.Parse domain
info.SubDomain |> Option.ofObj
let subdomainMarker =
match globalSubdomain with
| None -> ""
| Some s -> $".{s}"
let cnames =
cnames
|> Map.toSeq
|> Seq.map (fun (cname, target) ->
let source = $"{cname.ToString ()}{subdomainMarker}"
let target = WellKnownCnameTarget.Reify domain target
let args = RecordArgs ()
args.ZoneId <- Input.lift zone
args.Name <- Input.lift source
args.Ttl <- Input.lift 60
args.Type <- Input.lift "CNAME"
args.Value <- Input.lift target
source,
{
Record = Record ($"{cname}{subdomainMarker}-cname", args)
Source = source
Target = target
}
|> DnsRecord.Cname
)
|> Seq.toList
let subdomains =
subdomains
|> Seq.map (fun subdomainType ->
let subdomain = subdomainType.ToString ()
subdomain, DnsRecord.ARecord (makeARecord zone $"{subdomain}{subdomainMarker}" ipAddress)
)
|> Seq.toList
(domain.ToString (), DnsRecord.ARecord (makeARecord zone (domain.ToString ()) ipAddress))
:: cnames
@ subdomains
|> Map.ofList

View File

@@ -0,0 +1,93 @@
namespace PulumiWebServer
open System.IO
open Pulumi
open Pulumi.Command.Remote
[<RequireQualifiedAccess>]
module Command =
let deleteBeforeReplace =
CustomResourceOptions (DeleteBeforeReplace = System.Nullable true)
let createSecretFile (args : CommandArgs) (username : string) (toWrite : BashString) (filePath : string) : unit =
if filePath.Contains "'" then
failwith $"filepath contained quote: {filePath}"
if username.Contains "'" then
failwith $"username contained quote: {username}"
let argsString =
$"""OLD_UMASK=$(umask) && \
umask 077 && \
mkdir -p "$(dirname {filePath})" && \
echo {toWrite} > '{filePath}' && \
chown '{username}' '{filePath}' && \
umask "$OLD_UMASK"
"""
args.Create <- Input.ofOutput (Output.CreateSecret argsString)
args.Delete <- $"rm -f '{filePath}'"
let connection (privateKey : FileInfo) (address : Address) =
let inputArgs = Inputs.ConnectionArgs ()
inputArgs.Host <-
address.IPv4
|> Option.defaultWith (fun () -> Option.get address.IPv6)
|> Input.lift
inputArgs.Port <- Input.lift 22
inputArgs.User <- Input.lift "root"
inputArgs.PrivateKey <- File.ReadAllText privateKey.FullName |> Output.CreateSecret |> Input.ofOutput
inputArgs |> Output.CreateSecret |> Input.ofOutput
let contentAddressedCopy
(PrivateKey privateKey)
(address : Address)
(name : string)
(trigger : Output<'a>)
(targetPath : string)
(fileContents : string)
: Command
=
let args = CommandArgs ()
args.Connection <- connection privateKey address
args.Triggers <- trigger |> Output.map (unbox<obj> >> Seq.singleton) |> InputList.ofOutput
// TODO - do this by passing into stdin instead
if targetPath.Contains '\'' || targetPath.Contains '\n' then
failwith $"Can't copy a file to a location with a quote mark in, got: {targetPath}"
let delimiter = "EOF"
if fileContents.Contains delimiter then
failwith "String contained delimiter; please implement something better"
let commandString =
[
$"mkdir -p \"$(dirname {targetPath})\" && \\"
"{"
$"cat <<'{delimiter}'"
fileContents
delimiter
sprintf "} | tee '%s'" targetPath
]
|> String.concat "\n"
|> Output.CreateSecret
args.Create <- commandString
args.Delete <- $"rm -f '{targetPath}'"
Command (name, args, deleteBeforeReplace)
let addToNixFileCommand (args : CommandArgs) (filename : string) : unit =
args.Create <-
$"""while ! ls /preserve/nixos/configuration.nix; do sleep 5; done
sed -i '4i\
./{filename}' /preserve/nixos/configuration.nix"""
args.Delete <- $"""sed -i -n '/{filename}/!p' /preserve/nixos/configuration.nix || exit 0"""

View File

@@ -0,0 +1,197 @@
namespace PulumiWebServer
open System
open System.Collections.Generic
open System.IO
open Newtonsoft.Json
[<NoComparison>]
type Configuration =
{
/// Name of this server, as it will be known to Pulumi.
/// This isn't e.g. a hostname or anything; it's the key on which Pulumi deduplicates
/// different runs of this plan.
Name : string
/// Private key with which to talk to the server
PrivateKey : PrivateKey
/// Public key corresponding to the PrivateKey (default has ".pub" appended)
PublicKeyOverride : PublicKey option
/// Email address to which Let's Encrypt is to send emails
AcmeEmail : EmailAddress
/// Umbrella domain name for all services
Domain : DomainName
/// All cnames to be created in DNS
Cnames : Map<WellKnownCname, WellKnownCnameTarget>
/// All subdomains which are not cnames;
/// e.g. (WellKnownSubdomain.Www, "www") would indicate
/// the `www.domain.name` address, in the counterfactual
/// world where `Www` were implemented as a subdomain
/// and not a cname
Subdomains : Set<WellKnownSubdomain>
/// Linux user to create on the server
RemoteUsername : Username
GiteaConfig : GiteaConfig option
RadicaleConfig : RadicaleConfig option
}
member this.NginxConfig =
{
Domain = this.Domain
WebSubdomain = WellKnownCname.Www
AcmeEmail = this.AcmeEmail
}
member this.PublicKey =
match this.PublicKeyOverride with
| Some k -> k
| None ->
let (PrivateKey k) = this.PrivateKey
Path.Combine (k.Directory.FullName, k.Name + ".pub") |> FileInfo |> PublicKey
[<RequireQualifiedAccess>]
[<Struct>]
type SerialisedGiteaConfig =
{
[<JsonProperty(Required = Required.Always)>]
ServerPassword : string
[<JsonProperty(Required = Required.Always)>]
AdminPassword : string
[<JsonProperty(Required = Required.Always)>]
AdminUsername : string
[<JsonProperty(Required = Required.Always)>]
AdminEmailAddress : string
}
static member Make (config : GiteaConfig) =
{
SerialisedGiteaConfig.ServerPassword = config.ServerPassword |> BashString.unsafeOriginal
AdminPassword = config.AdminPassword |> BashString.unsafeOriginal
AdminUsername = config.AdminUsername |> BashString.unsafeOriginal
AdminEmailAddress = config.AdminEmailAddress |> BashString.unsafeOriginal
}
static member Deserialise (config : SerialisedGiteaConfig) : GiteaConfig =
{
GiteaConfig.ServerPassword = config.ServerPassword |> BashString.make
AdminPassword = config.AdminPassword |> BashString.make
AdminUsername = config.AdminUsername |> BashString.make
AdminEmailAddress = config.AdminEmailAddress |> BashString.make
}
[<RequireQualifiedAccess>]
[<Struct>]
type SerialisedRadicaleConfig =
{
[<JsonProperty(Required = Required.Always)>]
User : string
[<JsonProperty(Required = Required.Always)>]
Password : string
[<JsonProperty(Required = Required.DisallowNull)>]
GitEmail : string
}
static member Make (config : RadicaleConfig) =
{
SerialisedRadicaleConfig.User = config.User
Password = config.Password
GitEmail = config.GitEmail |> Option.toObj
}
static member Deserialise (c : SerialisedRadicaleConfig) : RadicaleConfig =
{
RadicaleConfig.User = c.User
Password = c.Password
GitEmail = c.GitEmail |> Option.ofObj
}
[<NoComparison>]
[<RequireQualifiedAccess>]
type SerialisedConfig =
{
[<JsonProperty(Required = Required.Always)>]
Name : string
/// Path to private key
[<JsonProperty(Required = Required.Always)>]
PrivateKey : string
/// Path to public key
[<JsonProperty(Required = Required.DisallowNull)>]
PublicKey : string
[<JsonProperty(Required = Required.Always)>]
AcmeEmail : string
[<JsonProperty(Required = Required.Always)>]
Domain : string
[<JsonProperty(Required = Required.Always)>]
Cnames : Dictionary<string, string>
[<JsonProperty(Required = Required.DisallowNull)>]
Subdomains : string[]
[<JsonProperty(Required = Required.Always)>]
RemoteUsername : string
GiteaConfig : Nullable<SerialisedGiteaConfig>
RadicaleConfig : Nullable<SerialisedRadicaleConfig>
}
static member Make (config : Configuration) =
{
SerialisedConfig.PrivateKey = let (PrivateKey p) = config.PrivateKey in p.FullName
Name = config.Name
PublicKey =
match config.PublicKeyOverride with
| None -> null
| Some (PublicKey p) -> p.FullName
AcmeEmail = config.AcmeEmail.ToString ()
Domain = config.Domain.ToString ()
Cnames =
config.Cnames
|> Map.toSeq
|> Seq.map (fun (cname, target) ->
KeyValuePair (cname.ToString (), WellKnownCnameTarget.Serialise target)
)
|> Dictionary
Subdomains = config.Subdomains |> Seq.map (fun sub -> sub.ToString ()) |> Seq.toArray
RemoteUsername = config.RemoteUsername.ToString ()
GiteaConfig = config.GiteaConfig |> Option.map SerialisedGiteaConfig.Make |> Option.toNullable
RadicaleConfig =
config.RadicaleConfig
|> Option.map SerialisedRadicaleConfig.Make
|> Option.toNullable
}
static member Deserialise (config : SerialisedConfig) : Configuration =
{
Configuration.PrivateKey = FileInfo config.PrivateKey |> PrivateKey
Name = config.Name
PublicKeyOverride =
match config.PublicKey with
| null -> None
| key -> FileInfo key |> PublicKey |> Some
AcmeEmail = config.AcmeEmail |> EmailAddress
Domain = config.Domain |> DomainName
Cnames =
config.Cnames
|> Seq.map (fun (KeyValue (cname, target)) ->
WellKnownCname.Parse cname, WellKnownCnameTarget.Deserialise target
)
|> Map.ofSeq
Subdomains =
match config.Subdomains with
| null -> Set.empty
| subdomains -> subdomains |> Seq.map WellKnownSubdomain.Parse |> Set.ofSeq
RemoteUsername = config.RemoteUsername |> Username
GiteaConfig =
config.GiteaConfig
|> Option.ofNullable
|> Option.map SerialisedGiteaConfig.Deserialise
RadicaleConfig =
config.RadicaleConfig
|> Option.ofNullable
|> Option.map SerialisedRadicaleConfig.Deserialise
}
[<RequireQualifiedAccess>]
module Configuration =
let get (configFile : Stream) : Configuration =
use reader = new StreamReader (configFile)
JsonConvert.DeserializeObject<SerialisedConfig> (reader.ReadToEnd ())
|> SerialisedConfig.Deserialise

View File

@@ -0,0 +1,42 @@
namespace PulumiWebServer
open Pulumi
open System.IO
open Pulumi.DigitalOcean
open Pulumi.DigitalOcean.Outputs
[<RequireQualifiedAccess>]
module DigitalOcean =
let saveSshKey (PublicKey publicKey) : SshKey =
let args = SshKeyArgs ()
args.PublicKey <- File.ReadAllText publicKey.FullName |> Input.lift
SshKey ("default", args)
let makeNixosServer (name : string) (region : Region) (sshKeys : Input<SshFingerprint>[]) : Output<Droplet> =
output {
let args =
DropletArgs (Name = Input.lift name, Size = InputUnion.liftRight DropletSlug.DropletS1VCPU1GB)
args.Tags.Add (Input.lift "nixos")
args.Image <- "ubuntu-22-04-x64" |> Input.lift
args.Monitoring <- Input.lift false
args.Backups <- Input.lift false
args.Ipv6 <- true
args.Region <- InputUnion.liftRight region
args.DropletAgent <- Input.lift false
args.GracefulShutdown <- Input.lift false
args.SshKeys.Add (sshKeys |> Array.map (Input.map (fun (SshFingerprint s) -> s)))
return Droplet (name, args)
}
let storedSshKeys (dep : 'a Output) : Output<GetSshKeysSshKeyResult list> =
let args = GetSshKeysInvokeArgs ()
output {
let! _ = dep
let! keys = GetSshKeys.Invoke args
return keys.SshKeys |> Seq.toList |> List.sortBy (fun s -> s.Fingerprint)
}

141
PulumiWebServer/Domain.fs Normal file
View File

@@ -0,0 +1,141 @@
namespace PulumiWebServer
open System.IO
type ZoneId = | ZoneId of string
[<NoComparison ; CustomEquality>]
type PublicKey =
| PublicKey of FileInfo
override this.Equals (other : obj) =
match this, other with
| PublicKey this, (:? PublicKey as PublicKey other) -> this.FullName = other.FullName
| _, _ -> false
override this.GetHashCode () =
match this with
| PublicKey p -> p.FullName.GetHashCode ()
[<NoComparison ; CustomEquality>]
type PrivateKey =
| PrivateKey of FileInfo
override this.Equals (other : obj) =
match this, other with
| PrivateKey this, (:? PrivateKey as PrivateKey other) -> this.FullName = other.FullName
| _, _ -> false
override this.GetHashCode () =
match this with
| PrivateKey p -> p.FullName.GetHashCode ()
type Username =
| Username of string
override this.ToString () =
match this with
| Username s -> s
type SshFingerprint = | SshFingerprint of string
type SshKey =
{
PublicKeyContents : string
Fingerprint : SshFingerprint
}
type EmailAddress =
| EmailAddress of string
override this.ToString () =
match this with
| EmailAddress s -> s
[<RequireQualifiedAccess>]
module SshKey =
let fingerprint (key : SshKey) = key.Fingerprint
type DomainName =
| DomainName of string
override this.ToString () =
match this with
| DomainName s -> s
type Address =
{
IPv4 : string option
IPv6 : string option
}
member this.Get () =
// TODO: default to IPv6 for access
match this.IPv4 with
| Some v -> v
| None ->
match this.IPv6 with
| Some v -> v
| None -> failwith "could not get"
override this.ToString () =
let ipv4 =
match this.IPv4 with
| Some s -> s
| None -> ""
let ipv6 =
match this.IPv6 with
| Some s -> s
| None -> ""
[ ipv4 ; ipv6 ] |> String.concat " ; "
type WellKnownSubdomain =
| Nextcloud
| Gitea
| Radicale
override this.ToString () =
match this with
| Nextcloud -> "nextcloud"
| Gitea -> "gitea"
| Radicale -> "calendar"
static member Parse (s : string) =
match s with
| "nextcloud" -> WellKnownSubdomain.Nextcloud
| "gitea" -> WellKnownSubdomain.Gitea
| "calendar" -> WellKnownSubdomain.Radicale
| _ -> failwith $"Failed to deserialise: {s}"
type WellKnownCnameTarget =
| Root
static member Reify (DomainName domain) (target : WellKnownCnameTarget) : string =
match target with
| WellKnownCnameTarget.Root -> domain
static member Serialise (t : WellKnownCnameTarget) : string =
match t with
| WellKnownCnameTarget.Root -> "root"
static member Deserialise (t : string) : WellKnownCnameTarget =
match t with
| "root" -> WellKnownCnameTarget.Root
| _ -> failwith $"Failed to deserialise: {t}"
type WellKnownCname =
| Www
override this.ToString () =
match this with
| Www -> "www"
static member Parse (s : string) =
match s with
| "www" -> WellKnownCname.Www
| _ -> failwith $"Failed to deserialise: {s}"

90
PulumiWebServer/Gitea.fs Normal file
View File

@@ -0,0 +1,90 @@
namespace PulumiWebServer
open Pulumi
open Pulumi.Command.Remote
[<RequireQualifiedAccess>]
type GiteaConfig =
{
ServerPassword : BashString
AdminPassword : BashString
AdminUsername : BashString
AdminEmailAddress : BashString
}
[<RequireQualifiedAccess>]
module Gitea =
let private writeConfig
(trigger : Output<'a>)
(DomainName domain)
(privateKey : PrivateKey)
(address : Address)
(config : GiteaConfig)
: Command
=
let giteaConfig =
Utils.getEmbeddedResource typeof<PrivateKey>.Assembly "gitea.nix"
|> fun s -> s.Replace ("@@DOMAIN@@", domain)
|> fun s -> s.Replace ("@@GITEA_SUBDOMAIN@@", WellKnownSubdomain.Gitea.ToString ())
|> fun s -> s.Replace ("@@GITEA_ADMIN_USERNAME@@", config.AdminUsername.ToString ())
|> fun s -> s.Replace ("@@GITEA_ADMIN_EMAIL@@", config.AdminEmailAddress.ToString ())
Command.contentAddressedCopy
privateKey
address
"write-gitea-config"
trigger
"/preserve/nixos/gitea.nix"
giteaConfig
let private loadConfig<'a>
(onChange : Output<'a>)
(PrivateKey privateKey as pk)
(address : Address)
(config : GiteaConfig)
: Command list
=
let loadNix =
let args = CommandArgs ()
args.Triggers <- onChange |> Output.map (unbox<obj> >> Seq.singleton) |> InputList.ofOutput
args.Connection <- Command.connection privateKey address
Command.addToNixFileCommand args "gitea.nix"
Command ("configure-gitea", args, Command.deleteBeforeReplace)
let writePassword =
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
Command.createSecretFile args "root" config.ServerPassword "/preserve/keys/gitea-db-pass"
Command ("configure-gitea-password", args, Command.deleteBeforeReplace)
let writeGiteaUserPassword =
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
Command.createSecretFile args "root" config.AdminPassword "/preserve/keys/gitea-admin-pass"
Command ("write-gitea-password", args, Command.deleteBeforeReplace)
[ loadNix ; writePassword ; writeGiteaUserPassword ]
let configure<'a>
(infectNixTrigger : Output<'a>)
(domain : DomainName)
(privateKey : PrivateKey)
(address : Address)
(config : GiteaConfig)
: Module
=
let writeConfig = writeConfig infectNixTrigger domain privateKey address config
{
WriteConfigFile = writeConfig
EnableConfig = loadConfig writeConfig.Stdout privateKey address config
}

View File

@@ -0,0 +1,34 @@
namespace PulumiWebServer
open System.Diagnostics
[<RequireQualifiedAccess>]
module Htpasswd =
/// Return the contents of an htpasswd file
let generate (username : string) (password : string) : string =
let args = ProcessStartInfo ()
args.FileName <- "htpasswd"
args.RedirectStandardOutput <- true
args.RedirectStandardError <- true
args.RedirectStandardInput <- true
args.UseShellExecute <- false
args.Arguments <- $"-n -i -B {username}"
use p = new Process ()
p.StartInfo <- args
if not <| p.Start () then
failwith "failed to start htpasswd"
p.StandardInput.Write password
p.StandardInput.Close ()
p.WaitForExit ()
if p.ExitCode = 0 then
p.StandardOutput.ReadToEnd ()
else
printfn $"{p.StandardError.ReadToEnd ()}"
failwith $"Bad exit code from htpasswd: {p.ExitCode}"

19
PulumiWebServer/Local.fs Normal file
View File

@@ -0,0 +1,19 @@
namespace PulumiWebServer
open System.Diagnostics
[<RequireQualifiedAccess>]
module Local =
let forgetKey (address : Address) : unit =
let address = address.Get ()
let psi = ProcessStartInfo "/usr/bin/ssh-keygen"
psi.Arguments <- $"-R {address}"
psi.RedirectStandardError <- true
psi.RedirectStandardOutput <- true
psi.UseShellExecute <- false
let proc = psi |> Process.Start
proc.WaitForExit ()
let error = proc.StandardOutput.ReadToEnd ()
// We don't expect to have configured SSH yet, so this is fine.
if proc.ExitCode <> 0 then
failwith $"Unexpectedly failed to forget key: {address} ({proc.ExitCode}). {error}"

13
PulumiWebServer/Module.fs Normal file
View File

@@ -0,0 +1,13 @@
namespace PulumiWebServer
open Pulumi.Command.Remote
type Module =
{
/// This is expected to be able to run in parallel with any
/// other Module.
WriteConfigFile : Command
/// This is expected to be able to run in parallel with any
/// other Module. TODO actually it's not?
EnableConfig : Command list
}

78
PulumiWebServer/Nginx.fs Normal file
View File

@@ -0,0 +1,78 @@
namespace PulumiWebServer
open Pulumi
open Pulumi.Command.Remote
type NginxConfig =
{
Domain : DomainName
WebSubdomain : WellKnownCname
AcmeEmail : EmailAddress
}
member this.Domains =
[ this.WebSubdomain ]
|> List.map (fun subdomain -> $"%O{subdomain}.{this.Domain}")
|> fun subdomains -> this.Domain.ToString () :: subdomains
[<RequireQualifiedAccess>]
module Nginx =
let private createNixConfig (config : NginxConfig) : string =
let configTemplate =
Utils.getEmbeddedResource typeof<NginxConfig>.Assembly "nginx.nix"
|> fun s ->
s
.Replace("@@DOMAIN@@", config.Domain.ToString ())
.Replace("@@WEBROOT_SUBDOMAIN@@", config.WebSubdomain.ToString ())
.Replace ("@@ACME_EMAIL@@", config.AcmeEmail.ToString ())
let certConfig =
config.Domains
|> List.map (fun domain ->
[
$"\"{domain}\" ="
"{"
" server = \"https://acme-v02.api.letsencrypt.org/directory\";"
"};"
]
|> String.concat "\n"
)
|> String.concat "\n"
configTemplate.Replace ("\"@@DOMAINS@@\"", sprintf "{%s}" certConfig)
let private loadConfig (onChange : Output<'a>) (PrivateKey privateKey) (address : Address) =
let args = CommandArgs ()
args.Triggers <- InputList.ofOutput<obj> (onChange |> Output.map (unbox<obj> >> Seq.singleton))
args.Connection <- Command.connection privateKey address
Command.addToNixFileCommand args "nginx.nix"
Command ("configure-nginx", args, Command.deleteBeforeReplace)
let private writeConfig
(trigger : Output<'a>)
(nginxConfig : NginxConfig)
(privateKey : PrivateKey)
(address : Address)
: Command
=
let nginx = createNixConfig nginxConfig
Command.contentAddressedCopy privateKey address "write-nginx-config" trigger "/preserve/nixos/nginx.nix" nginx
let configure<'a>
(infectNixTrigger : Output<'a>)
(privateKey : PrivateKey)
(address : Address)
(config : NginxConfig)
: Module
=
let writeConfig = writeConfig infectNixTrigger config privateKey address
{
WriteConfigFile = writeConfig
EnableConfig = loadConfig writeConfig.Stdout privateKey address |> List.singleton
}

64
PulumiWebServer/Nix/flake.lock generated Normal file
View File

@@ -0,0 +1,64 @@
{
"nodes": {
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"utils": "utils"
},
"locked": {
"lastModified": 1672349765,
"narHash": "sha256-Ul3lSGglgHXhgU3YNqsNeTlRH1pqxbR64h+2hM+HtnM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "dd99675ee81fef051809bc87d67eb07f5ba022e8",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1672262501,
"narHash": "sha256-ZNXqX9lwYo1tOFAqrVtKTLcJ2QMKCr3WuIvpN8emp7I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e182da8622a354d44c39b3d7a542dc12cd7baa5f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"home-manager": "home-manager",
"nixpkgs": "nixpkgs"
}
},
"utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,23 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
nixpkgs,
home-manager,
}: {
nixosConfigurations.nixos-server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
];
};
nix.registry.nixpkgs.flake = nixpkgs;
};
}

View File

@@ -0,0 +1,115 @@
{
config,
pkgs,
...
}: let
port = 3001;
in {
services.gitea = {
enable = true;
appName = "Gitea";
lfs.enable = true;
stateDir = "/preserve/gitea";
database = {
type = "postgres";
passwordFile = "/preserve/gitea/gitea-db-pass";
};
domain = "@@GITEA_SUBDOMAIN@@.@@DOMAIN@@";
rootUrl = "https://@@GITEA_SUBDOMAIN@@.@@DOMAIN@@/";
httpPort = port;
settings = let
docutils = pkgs.python37.withPackages (ps:
with ps; [
docutils
pygments
]);
in {
mailer = {
ENABLED = true;
FROM = "gitea@" + "@@DOMAIN@@";
};
service = {
REGISTER_EMAIL_CONFIRM = true;
DISABLE_REGISTRATION = true;
COOKIE_SECURE = true;
};
"markup.restructuredtext" = {
ENABLED = true;
FILE_EXTENSIONS = ".rst";
RENDER_COMMAND = ''${docutils}/bin/rst2html.py'';
IS_INPUT_FILE = false;
};
};
};
services.postgresql = {
enable = true;
# TODO: make this use the /preserve mount
# dataDir = "/preserve/postgresql/data";
authentication = ''
local gitea all ident map=gitea-users
'';
identMap = ''
gitea-users gitea gitea
'';
};
services.nginx.virtualHosts."@@GITEA_SUBDOMAIN@@.@@DOMAIN@@" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:${toString port}/";
};
};
systemd.services.gitea-supply-password = {
description = "gitea-supply-password";
wantedBy = ["gitea.service"];
path = [pkgs.gitea];
script = ''
mkdir -p /preserve/gitea && \
chown -R gitea /preserve/gitea && \
ln -f /preserve/keys/gitea-admin-pass /preserve/gitea/gitea-admin-pass && \
chown gitea /preserve/gitea/gitea-admin-pass && \
ln -f /preserve/keys/gitea-db-pass /preserve/gitea/gitea-db-pass && \
chown gitea /preserve/gitea/gitea-db-pass
'';
serviceConfig = {
Restart = "no";
Type = "oneshot";
User = "root";
Group = "root";
};
};
# The Gitea module does not allow adding users declaratively
systemd.services.gitea-add-user = {
description = "gitea-add-user";
after = ["gitea-supply-password.service"];
wantedBy = ["multi-user.target"];
path = [pkgs.gitea];
script = '' TMPFILE=$(mktemp)
PASSWORD=$(cat /preserve/gitea/gitea-admin-pass)
set +e
${pkgs.gitea} migrate -c /preserve/gitea/data/custom/conf/app.ini
${pkgs.gitea}/bin/gitea admin user create --admin --username @@GITEA_ADMIN_USERNAME@@ --password "$PASSWORD" --email @@GITEA_ADMIN_EMAIL@@ 2>"$TMPFILE" 1>"$TMPFILE"
EXITCODE=$?
if [ $EXITCODE -eq 1 ]; then
if grep 'already exists' "$TMPFILE" 2>/dev/null 1>/dev/null; then
EXITCODE=0
fi
fi
cat "$TMPFILE"
rm "$TMPFILE"
exit $EXITCODE
'';
serviceConfig = {
Restart = "no";
Type = "oneshot";
User = "gitea";
Group = "gitea";
WorkingDirectory = config.services.gitea.stateDir;
};
environment = {GITEA_WORK_DIR = config.services.gitea.stateDir;};
};
}

View File

@@ -0,0 +1,39 @@
{...}: let
domain = "@@DOMAIN@@";
in {
security.acme.acceptTerms = true;
security.acme.defaults.email = "@@ACME_EMAIL@@";
security.acme.certs = "@@DOMAINS@@";
networking.firewall.allowedTCPPorts = [
80 # required for the ACME challenge
443
];
services.nginx = {
enable = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
virtualHosts."${domain}" = {
globalRedirect = "@@WEBROOT_SUBDOMAIN@@.${domain}";
addSSL = true;
enableACME = true;
root = "/preserve/www/html";
};
virtualHosts."@@WEBROOT_SUBDOMAIN@@.${domain}" = {
addSSL = true;
enableACME = true;
root = "/preserve/www/html";
extraConfig = ''
location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2)$ {
expires 30d;
add_header Pragma public;
add_header Cache-Control "public";
}
'';
};
};
}

View File

@@ -0,0 +1,32 @@
{pkgs, ...}: let
port = 5232;
enableGit = true;
storage =
if enableGit
then {
hook = "${pkgs.git}/bin/git add -A && (${pkgs.git}/bin/git diff --cached --quiet || ${pkgs.git}/bin/git commit -m 'Changes by '%(user)s)";
filesystem_folder = "/preserve/radicale/data";
}
else {};
in {
services.radicale = {
enable = true;
settings = {
server.hosts = ["0.0.0.0:${toString port}"];
auth = {
type = "htpasswd";
htpasswd_filename = "/preserve/keys/radicale-users";
htpasswd_encryption = "bcrypt";
};
storage = storage;
};
};
services.nginx.virtualHosts."@@RADICALE_SUBDOMAIN@@.@@DOMAIN@@" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:${toString port}/";
};
};
}

View File

@@ -0,0 +1,34 @@
{pkgs, ...}: {
users.mutableUsers = false;
users.users."@@USER@@" = {
isNormalUser = true;
home = "/home/@@USER@@";
extraGroups = ["wheel"];
openssh.authorizedKeys.keys = ["@@AUTHORIZED_KEYS@@"];
};
security.sudo = {
enable = true;
extraRules = [
{
users = ["@@USER@@"];
commands = [
{
command = "ALL";
options = ["NOPASSWD"];
}
];
}
];
};
nix.extraOptions = ''
experimental-features = nix-command flakes
'';
environment.systemPackages = [
pkgs.vim
pkgs.git
pkgs.home-manager
];
}

171
PulumiWebServer/Program.fs Normal file
View File

@@ -0,0 +1,171 @@
namespace PulumiWebServer
open System
open Nager.PublicSuffix
open Pulumi
open Pulumi.DigitalOcean
open System.IO
module Program =
let stripSubdomain (DomainName str) =
let parser = DomainParser (WebTldRuleProvider ())
let info = parser.Parse str
$"{info.Domain}.{info.TLD}" |> DomainName
let config =
use file =
FileInfo("/Users/patrick/Documents/GitHub/WebsiteConfig/config.json")
.OpenRead ()
Configuration.get file
[<EntryPoint>]
let main _argv =
fun () ->
output {
let! existingKeys = DigitalOcean.storedSshKeys (Output.Create "")
let keyContents =
let (PublicKey file) = config.PublicKey
File.ReadAllText file.FullName
let key =
existingKeys
|> Seq.filter (fun key -> key.PublicKey = keyContents)
|> Seq.tryHead
let key =
match key with
| None -> (DigitalOcean.saveSshKey config.PublicKey).Name
| Some key -> Output.Create key.Name
let! keys =
DigitalOcean.storedSshKeys key
|> Output.map (
Seq.map (fun s ->
{
Fingerprint = SshFingerprint s.Fingerprint
PublicKeyContents = s.PublicKey
}
)
>> Seq.sort
>> Array.ofSeq
)
let! droplet =
keys
|> Array.map (SshKey.fingerprint >> Input.lift)
|> DigitalOcean.makeNixosServer "server-staging" Region.LON1
let! ipv4 = droplet.Ipv4Address
let! ipv6 = droplet.Ipv6Address
let address =
{
IPv4 = Option.ofObj ipv4
IPv6 = Option.ofObj ipv6
}
let! zone = Cloudflare.getZone (stripSubdomain config.Domain)
let dns =
Cloudflare.addDns config.Domain config.Cnames config.Subdomains zone address
let! _ = Server.waitForReady config.PrivateKey address
let deps =
let dnsDeps =
dns
|> Map.toList
|> List.collect (fun (_, record) ->
match record with
| DnsRecord.ARecord record -> [ record.IPv4 ; record.IPv6 ]
| DnsRecord.Cname _ -> []
)
|> List.choose id
|> List.map (fun record -> record.Urn)
dnsDeps |> Output.sequence |> Output.map (String.concat ",")
let! _ = deps
let infectNix = Server.infectNix config.PrivateKey address
let! _ = infectNix.Stdout
// The nixos rebuild has blatted the known public key.
Local.forgetKey address
let! _ = Server.waitForReady config.PrivateKey address
let initialSetupModules =
[
yield Server.configureUser infectNix.Stdout config.RemoteUsername keys config.PrivateKey address
yield! Server.writeFlake infectNix.Stdout config.PrivateKey address
]
let! _ =
initialSetupModules
|> Seq.map (fun m -> m.WriteConfigFile.Stdout)
|> Output.sequence
// Load the configuration
let setup =
initialSetupModules
|> Seq.map (fun m ->
m.EnableConfig
|> Seq.map (fun c -> c.Stdout)
|> Output.sequence
|> Output.map (String.concat "\n---\n")
)
|> Output.sequence
|> Output.map (String.concat "\n===\n")
let rebuild = Server.nixRebuild 0 setup config.PrivateKey address
let! _ = rebuild.Stdout
// If this is a new node, reboot
let firstReboot = Server.reboot "post-infect" droplet.Urn config.PrivateKey address
let! _ = firstReboot.Stdout
let! _ = Server.waitForReady config.PrivateKey address
let copyPreserve = Server.copyPreserve config.PrivateKey address
let! _ = copyPreserve.Stdout
let modules =
[
Nginx.configure copyPreserve.Stdout config.PrivateKey address config.NginxConfig
|> Some
config.GiteaConfig
|> Option.map (Gitea.configure copyPreserve.Stdout config.Domain config.PrivateKey address)
config.RadicaleConfig
|> Option.map (Radicale.configure copyPreserve.Stdout config.Domain config.PrivateKey address)
]
|> List.choose id
let configFiles =
modules |> Seq.map (fun m -> m.WriteConfigFile.Stdout) |> Output.sequence
// Wait for the config files to be written
let! _ = configFiles
// Load the configuration
let modules =
modules
|> Seq.map (fun m ->
m.EnableConfig
|> Seq.map (fun c -> c.Stdout)
|> Output.sequence
|> Output.map (String.concat "\n---\n")
)
|> Output.sequence
|> Output.map (String.concat "\n===\n")
let rebuild = Server.nixRebuild 1 modules config.PrivateKey address
let! _ = rebuild.Stdout
return ()
}
|> ignore
|> Deployment.RunAsync
|> Async.AwaitTask
|> Async.RunSynchronously

View File

@@ -0,0 +1,5 @@
config:
cloudflare:apiToken:
secure: AAABAOaQPcYG4jCFbYYr6r0dqR2f5csiAulm+GGu6EZeR1pVgqoVKUOHK3hmlW+FYUcXvnhs9Rpd9tQ15dIkplJdOp/2CEgv
digitalocean:token:
secure: AAABAAnqEO15oRMrB/9nBZaz+9ZLqo+OLz0k23QQFCS8eFgM45sGrUQIPoeCSWJ/tq+AThr8wjhe3qU6PJWxRD+zpLSHS2E/y+EH1o9WyPCi0eXeFY3uttp5ToDiVbCDiyCNVtUBwQ==

67
PulumiWebServer/Pulumi.fs Normal file
View File

@@ -0,0 +1,67 @@
namespace Pulumi
open Pulumi
[<RequireQualifiedAccess>]
module Input =
let lift<'a> (x : 'a) : 'a Input = Input.op_Implicit x
let ofOutput<'a> (x : 'a Output) : 'a Input = Input.op_Implicit x
let map<'a, 'b> (f : 'a -> 'b) (x : Input<'a>) : Input<'b> = x.Apply f |> ofOutput
[<RequireQualifiedAccess>]
module Output =
let map<'a, 'b> (f : 'a -> 'b) (x : 'a Output) : 'b Output = x.Apply f
let sequence<'a> (xs : 'a Output seq) : 'a list Output =
let func (o : 'a list Output) (x : 'a Output) : 'a list Output =
o.Apply<'a list> (fun o -> x.Apply<'a list> (fun x -> x :: o))
xs |> Seq.fold func (Output.Create []) |> map List.rev
type OutputEvaluator<'ret> =
abstract Eval<'a> : Output<'a> -> 'ret
type OutputCrate =
abstract Apply<'ret> : OutputEvaluator<'ret> -> 'ret
[<RequireQualifiedAccess>]
module OutputCrate =
let make<'a> (o : Output<'a>) =
{ new OutputCrate with
member _.Apply e = e.Eval o
}
// Yuck but this is the type signature we need for consumption by Pulumi
let sequence (xs : OutputCrate seq) : obj list Output =
let func (o : obj list Output) (x : OutputCrate) : obj list Output =
{ new OutputEvaluator<_> with
member _.Eval<'a> (x : 'a Output) =
o.Apply<obj list> (fun o -> x.Apply<obj list> (fun x -> unbox<obj> x :: o))
}
|> x.Apply
xs |> Seq.fold func (Output.Create []) |> Output.map List.rev
[<RequireQualifiedAccess>]
module InputList =
let ofOutput<'a> (x : 'a seq Output) : 'a InputList = InputList.op_Implicit x
let lift<'a> (x : 'a seq) : 'a InputList =
x |> Seq.toArray |> InputList.op_Implicit
[<RequireQualifiedAccess>]
module InputUnion =
let liftLeft<'a, 'b> (x : 'a) : InputUnion<'a, 'b> = InputUnion.op_Implicit x
let liftRight<'a, 'b> (x : 'b) : InputUnion<'a, 'b> = InputUnion.op_Implicit x
type OutputComputation () =
member _.Bind (x : Output<'a>, f : 'a -> Output<'b>) : Output<'b> = x.Apply<'b> f
member _.Return (x : 'a) : Output<'a> = Output.Create<'a> x
member _.ReturnFrom (x : 'a Output) = x
[<AutoOpen>]
module ComputationExpressions =
let output = OutputComputation ()

View File

@@ -0,0 +1,5 @@
config:
cloudflare:apiToken:
secure: AAABAK391jNLL3SDyFJBn/mBEdcZ7tUyJhwrRsdrHvckN+GzrBw5CJq4+ftaRRSIZEObTd/3wPFmoxcqgmIsiGAEBjHqLGak
digitalocean:token:
secure: AAABAIypnl37QdxXkzb8LIQvB26ncgvEjf8NgGx+KNe4rzJACTVCvvkxsf2lWG8Zf9uY2PO6WLk4qjIS6Mgm2SdQkEM1HgL2BYxyK+OGPNKb/ks9Dlw+TnkIZRVILyYlyqE7e5DRvg==

View File

@@ -0,0 +1,4 @@
name: PulumiWebServer
description: Pulumi configuration for my personal web server
runtime:
name: dotnet

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nager.PublicSuffix" Version="2.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Pulumi" Version="3.37.2" />
<PackageReference Include="Pulumi.Cloudflare" Version="4.9.0" />
<PackageReference Include="Pulumi.Command" Version="0.4.1" />
<PackageReference Include="Pulumi.DigitalOcean" Version="4.14.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Utils.fs" />
<Compile Include="Htpasswd.fs" />
<Compile Include="BashString.fsi" />
<Compile Include="BashString.fs" />
<Compile Include="Pulumi.fs" />
<Compile Include="Module.fs" />
<Compile Include="Command.fs" />
<Compile Include="Cloudflare.fs" />
<Compile Include="DigitalOcean.fs" />
<Compile Include="Nginx.fs" />
<Compile Include="Server.fs" />
<Compile Include="Gitea.fs" />
<Compile Include="Radicale.fs" />
<Compile Include="Local.fs" />
<Compile Include="Configuration.fs" />
<Compile Include="Program.fs" />
<EmbeddedResource Include="Nix\nginx.nix" />
<EmbeddedResource Include="Nix\userconfig.nix" />
<EmbeddedResource Include="Nix\gitea.nix" />
<EmbeddedResource Include="Nix\radicale.nix" />
<EmbeddedResource Include="Nix\flake.nix" />
<EmbeddedResource Include="Nix\flake.lock" />
<Content Include="config.schema.json" />
</ItemGroup>
</Project>

122
PulumiWebServer/Radicale.fs Normal file
View File

@@ -0,0 +1,122 @@
namespace PulumiWebServer
open Pulumi
open Pulumi.Command.Remote
type RadicaleConfig =
{
/// The user who will log in to the CalDAV server
User : string
/// The password for the user when they log in to the CalDAV server
Password : string
/// The email address for the Git user, if we are going to set up Git versioning.
GitEmail : string option
}
[<RequireQualifiedAccess>]
module Radicale =
let private loadConfig<'a>
(onChange : Output<'a>)
(PrivateKey privateKey as pk)
(address : Address)
(config : RadicaleConfig)
: Command list
=
let loadNix =
let args = CommandArgs ()
args.Triggers <- onChange |> Output.map (unbox<obj> >> Seq.singleton) |> InputList.ofOutput
args.Connection <- Command.connection privateKey address
Command.addToNixFileCommand args "radicale.nix"
Command ("configure-radicale", args, Command.deleteBeforeReplace)
let createUser = Server.createUser pk address (BashString.make "radicale")
let writePassword =
let password = Htpasswd.generate config.User config.Password |> BashString.make
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
args.Triggers <- createUser.Stdout |> Output.map (box >> Seq.singleton) |> InputList.ofOutput
Command.createSecretFile args "root" password "/preserve/keys/radicale-users"
Command ("configure-radicale-user", args, Command.deleteBeforeReplace)
let writeGit =
match config.GitEmail with
| None -> []
| Some gitEmail ->
let writeGitConfig =
$"""[user]
email = "%s{gitEmail}"
name = "radicale"
"""
|> Command.contentAddressedCopy
pk
address
"radicale-gitconfig"
onChange
"/preserve/radicale/data/.git/config"
let writeGitIgnore =
""".Radicale.cache
.Radicale.lock
.Radicale.tmp-*"""
|> Command.contentAddressedCopy
pk
address
"radicale-gitignore"
onChange
"/preserve/radicale/data/.gitignore"
[ writeGitConfig ; writeGitIgnore ]
[ yield loadNix ; yield writePassword ; yield! writeGit ]
let private writeConfig
(enableGit : bool)
(trigger : Output<'a>)
(DomainName domain)
(privateKey : PrivateKey)
(address : Address)
: Command
=
let radicaleConfig =
Utils.getEmbeddedResource typeof<PrivateKey>.Assembly "radicale.nix"
|> fun s -> s.Replace ("@@DOMAIN@@", domain)
|> fun s -> s.Replace ("@@RADICALE_SUBDOMAIN@@", WellKnownSubdomain.Radicale.ToString ())
|> fun s ->
if not enableGit then
s.Replace ("enableGit = true", "enableGit = false")
else
s
Command.contentAddressedCopy
privateKey
address
"write-radicale-config"
trigger
"/preserve/nixos/radicale.nix"
radicaleConfig
let configure
(infectNixTrigger : Output<'a>)
(domain : DomainName)
(privateKey : PrivateKey)
(address : Address)
(config : RadicaleConfig)
: Module
=
let writeConfig =
writeConfig config.GitEmail.IsSome infectNixTrigger domain privateKey address
{
WriteConfigFile = writeConfig
EnableConfig = loadConfig writeConfig.Stdout privateKey address config
}

176
PulumiWebServer/Server.fs Normal file
View File

@@ -0,0 +1,176 @@
namespace PulumiWebServer
open System
open System.Diagnostics
open Pulumi
open Pulumi.Command.Remote
[<RequireQualifiedAccess>]
module Server =
let createUser (PrivateKey privateKey) (address : Address) (name : BashString) =
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
args.Create <- $"useradd --no-create-home --no-user-group {name} 2>/dev/null 1>/dev/null || echo {name}"
Command ($"create-user-{name}", args)
let rec waitForReady (PrivateKey privateKey as pk) (address : Address) : Output<unit> =
output {
let psi = ProcessStartInfo "/usr/bin/ssh"
psi.Arguments <-
$"root@{address.Get ()} -o ConnectTimeout=5 -o IdentityFile={privateKey.FullName} -o StrictHostKeyChecking=off echo hello"
psi.RedirectStandardError <- true
psi.RedirectStandardOutput <- true
psi.UseShellExecute <- false
let proc = psi |> Process.Start
proc.WaitForExit ()
let output = proc.StandardOutput.ReadToEnd ()
let error = proc.StandardOutput.ReadToEnd ()
// We don't expect to have configured SSH yet, so this is fine.
if proc.ExitCode = 0 && output.StartsWith "hello" then
// For some reason /usr/bin/ssh can get in at this point even though Pulumi cannot :(
// error: ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain
System.Threading.Thread.Sleep (TimeSpan.FromSeconds 10.0)
return ()
else
printfn $"Sleeping due to: {proc.ExitCode} {error}"
System.Threading.Thread.Sleep (TimeSpan.FromSeconds 5.0)
return! waitForReady pk address
}
let infectNix (PrivateKey privateKey) (address : Address) =
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
// IMPORTANT NOTE: do not inline this script. It is licensed under the GPL, so we
// must invoke it without "establishing intimate communication" with it.
// https://www.gnu.org/licenses/gpl-faq.html#GPLPlugins
args.Create <-
"""if ! ls /run/current-system 1>/dev/null; then
curl https://raw.githubusercontent.com/elitak/nixos-infect/318fc516d1d87410fd06178331a9b2939b9f2fef/nixos-infect > /tmp/infect.sh || exit 1
while ! NO_REBOOT=1 PROVIDER=digitalocean NIX_CHANNEL=nixos-22.05 bash /tmp/infect.sh 2>&1 1>/tmp/infect.log; do
sleep 5;
done
fi && mkdir -p /preserve/nixos && cp /etc/nixos/* /preserve/nixos && touch /preserve/ready.txt && date"""
Command ("nix-infect", args)
let writeFlake (trigger : Output<'a>) (privateKey : PrivateKey) (address : Address) =
let flakeFile = Utils.getEmbeddedResource typeof<PrivateKey>.Assembly "flake.nix"
let flakeLock = Utils.getEmbeddedResource typeof<PrivateKey>.Assembly "flake.lock"
[
{
WriteConfigFile =
Command.contentAddressedCopy
privateKey
address
"write-flake"
trigger
"/preserve/nixos/flake.nix"
flakeFile
EnableConfig = []
}
{
WriteConfigFile =
Command.contentAddressedCopy
privateKey
address
"write-flake-lock"
trigger
"/preserve/nixos/flake.lock"
flakeLock
EnableConfig = []
}
]
let private writeUserConfig
(trigger : Output<'a>)
(keys : SshKey seq)
(Username username)
(privateKey : PrivateKey)
(address : Address)
: Command
=
let keys =
keys
|> Seq.collect (fun k -> k.PublicKeyContents.Split '\n')
|> Seq.filter (not << String.IsNullOrEmpty)
let userConfig =
Utils.getEmbeddedResource typeof<PrivateKey>.Assembly "userconfig.nix"
|> fun s ->
s
.Replace("@@AUTHORIZED_KEYS@@", keys |> String.concat "\" \"")
.Replace ("@@USER@@", username)
Command.contentAddressedCopy
privateKey
address
"write-user-config"
trigger
"/preserve/nixos/userconfig.nix"
userConfig
let private loadUserConfig (onChange : Output<'a>) (PrivateKey privateKey) (address : Address) =
let args = CommandArgs ()
args.Triggers <- onChange |> Output.map (unbox<obj> >> Seq.singleton) |> InputList.ofOutput
args.Connection <- Command.connection privateKey address
Command.addToNixFileCommand args "userconfig.nix"
Command ("configure-users", args, Command.deleteBeforeReplace)
let configureUser<'a>
(infectNixTrigger : Output<'a>)
(remoteUser : Username)
(keys : SshKey seq)
(privateKey : PrivateKey)
(address : Address)
: Module
=
let writeConfig =
writeUserConfig infectNixTrigger keys remoteUser privateKey address
{
WriteConfigFile = writeConfig
EnableConfig = loadUserConfig writeConfig.Stdout privateKey address |> List.singleton
}
let nixRebuild (counter : int) (onChange : Output<'a>) (PrivateKey privateKey) (address : Address) =
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
// The rebuild fails with exit code 1, indicating that we need to restart. This is fine.
args.Create <-
// TODO /nix/var/nix/profiles/system/sw/bin/nixos-rebuild might do it
"$(find /nix/store -type f -name nixos-rebuild | head -1) switch --flake /preserve/nixos#nixos-server || exit 0"
args.Triggers <- onChange |> Output.map (unbox<obj> >> Seq.singleton) |> InputList.ofOutput
Command ($"nixos-rebuild-{counter}", args)
let reboot (stage : string) (onChange : Output<'a>) (PrivateKey privateKey) (address : Address) =
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
args.Triggers <- InputList.ofOutput<obj> (onChange |> Output.map (unbox<obj> >> Seq.singleton))
args.Create <-
"while ! ls /preserve/ready.txt ; do sleep 10; done && rm -f /preserve/ready.txt && shutdown -r now"
Command ($"reboot-{stage}", args)
let copyPreserve (PrivateKey privateKey) (address : Address) =
let args = CommandArgs ()
args.Connection <- Command.connection privateKey address
args.Create <- "mkdir /preserve && cp -ar /old-root/preserve/nixos /preserve/nixos"
Command ("copy-preserve", args)

16
PulumiWebServer/Utils.fs Normal file
View File

@@ -0,0 +1,16 @@
namespace PulumiWebServer
open System.IO
open System.Reflection
[<RequireQualifiedAccess>]
module Utils =
let getEmbeddedResource (assembly : Assembly) (name : string) : string =
use s =
assembly.GetManifestResourceNames ()
|> Seq.filter (fun s -> s.EndsWith name)
|> Seq.exactlyOne
|> assembly.GetManifestResourceStream
|> fun s -> new StreamReader (s)
s.ReadToEnd ()

View File

@@ -0,0 +1,114 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "SerialisedConfig",
"type": "object",
"additionalProperties": false,
"required": [
"name",
"privateKey",
"acmeEmail",
"domain",
"cnames",
"remoteUsername"
],
"properties": {
"name": {
"type": "string"
},
"privateKey": {
"type": "string"
},
"publicKey": {
"type": "string"
},
"acmeEmail": {
"type": "string"
},
"domain": {
"type": "string"
},
"cnames": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"subdomains": {
"type": "array",
"items": {
"type": "string"
}
},
"remoteUsername": {
"type": "string"
},
"giteaConfig": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/SerialisedGiteaConfig"
}
]
},
"radicaleConfig": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/SerialisedRadicaleConfig"
}
]
}
},
"definitions": {
"SerialisedGiteaConfig": {
"type": "object",
"additionalProperties": false,
"required": [
"serverPassword",
"adminPassword",
"adminUsername",
"adminEmailAddress"
],
"properties": {
"serverPassword": {
"type": "string"
},
"adminPassword": {
"type": "string"
},
"adminUsername": {
"type": "string"
},
"adminEmailAddress": {
"type": "string"
}
}
},
"SerialisedRadicaleConfig": {
"type": "object",
"additionalProperties": false,
"required": [
"user",
"password"
],
"properties": {
"user": {
"type": "string"
},
"password": {
"type": "string"
},
"gitEmail": {
"type": [
"null",
"string"
]
}
}
}
}
}

16
README.md Normal file
View File

@@ -0,0 +1,16 @@
# Personal Pulumi configuration
## Development tips
There are pull request checks on this repo, enforcing [Fantomas](https://github.com/fsprojects/fantomas/)-compliant formatting on the F# code.
After checking out the repo, you may wish to add a pre-push hook to ensure locally that formatting is complete, rather than having to wait for the CI checks to tell you that you haven't formatted your code.
Consider performing the following command to set this up in the repo:
```bash
git config core.hooksPath hooks/
```
Before your first push (but only once), you will need to install the [.NET local tools](https://docs.microsoft.com/en-us/dotnet/core/tools/local-tools-how-to-use) which form part of the pre-push hook:
```bash
dotnet tool restore
```
You can format the Nix code with [Alejandra](https://github.com/kamadorueda/alejandra); simply `nix develop` and `alejandra .`.

35
configure.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/sh
cd PulumiWebServer || exit 1
exit_code=0
if [ -z "$DIGITAL_OCEAN_TOKEN" ]; then
echo "Get a Digital Ocean personal access token and pass it in as the env var DIGITAL_OCEAN_TOKEN."
exit_code=1
else
pulumi config set digitalocean:token "$DIGITAL_OCEAN_TOKEN" --secret
fi
if [ -z "$DIGITAL_OCEAN_SPACES_KEY" ]; then
echo "Get a Digital Ocean spaces key and pass it in as the env var DIGITAL_OCEAN_SPACES_KEY."
exit_code=1
else
pulumi config set digitalocean:spaces_access_id "$DIGITAL_OCEAN_SPACES_KEY" --secret
fi
if [ -z "$DIGITAL_OCEAN_SPACES_SECRET" ]; then
echo "Get a Digital Ocean spaces key and pass its secret in as the env var DIGITAL_OCEAN_SPACES_SECRET."
exit_code=1
else
pulumi config set digitalocean:spaces_secret_key "$DIGITAL_OCEAN_SPACES_SECRET" --secret
fi
if [ -z "$CLOUDFLARE_API_TOKEN" ]; then
echo "Get a Cloudflare API token with edit-DNS rights, and pass it in as the env var CLOUDFLARE_API_TOKEN."
exit_code=1
else
pulumi config set cloudflare:apiToken "$CLOUDFLARE_API_TOKEN" --secret
fi
exit $exit_code

42
flake.lock generated Normal file
View File

@@ -0,0 +1,42 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1672350804,
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

94
flake.nix Normal file
View File

@@ -0,0 +1,94 @@
{
description = "Web server flake";
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
...
}:
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = import nixpkgs {inherit system;};
projectFile = "./PulumiWebServer/PulumiWebServer.fsproj";
testProjectFile = "./PulumiWebServer.Test/PulumiWebServer.Test.fsproj";
pname = "PulumiWebServer";
dotnet-sdk = pkgs.dotnet-sdk_7;
dotnet-runtime = pkgs.dotnetCorePackages.runtime_7_0;
version = "0.0.1";
dotnetTool = toolName: toolVersion: sha256:
pkgs.stdenvNoCC.mkDerivation rec {
name = toolName;
version = toolVersion;
nativeBuildInputs = [pkgs.makeWrapper];
src = pkgs.fetchNuGet {
pname = name;
version = version;
sha256 = sha256;
installPhase = ''mkdir -p $out/bin && cp -r tools/net6.0/any/* $out/bin'';
};
installPhase = ''
runHook preInstall
mkdir -p "$out/lib"
cp -r ./bin/* "$out/lib"
makeWrapper "${dotnet-runtime}/bin/dotnet" "$out/bin/${name}" --add-flags "$out/lib/${name}.dll"
runHook postInstall
'';
};
in {
packages = {
fantomas = dotnetTool "fantomas" "5.2.0-alpha-010" "sha256-CuoROZBBhaK0IFjbKNLvzgX4GXwuIybqIvCtuqROBMk=";
fetchDeps = let
flags = [];
runtimeIds = map (system: pkgs.dotnetCorePackages.systemToDotnetRid system) dotnet-sdk.meta.platforms;
in
pkgs.writeShellScript "fetch-${pname}-deps" (builtins.readFile (pkgs.substituteAll {
src = ./nix/fetchDeps.sh;
pname = pname;
binPath = pkgs.lib.makeBinPath [pkgs.coreutils dotnet-sdk (pkgs.nuget-to-nix.override {inherit dotnet-sdk;})];
projectFiles = toString (pkgs.lib.toList projectFile);
testProjectFiles = toString (pkgs.lib.toList testProjectFile);
rids = pkgs.lib.concatStringsSep "\" \"" runtimeIds;
packages = dotnet-sdk.packages;
storeSrc = pkgs.srcOnly {
src = ./.;
pname = pname;
version = version;
};
}));
default = pkgs.buildDotnetModule {
pname = pname;
version = version;
src = ./.;
projectFile = projectFile;
nugetDeps = ./nix/deps.nix;
doCheck = true;
dotnet-sdk = dotnet-sdk;
dotnet-runtime = dotnet-runtime;
};
};
devShells = let
requirements = [pkgs.dotnet-sdk_7 pkgs.git pkgs.alejandra pkgs.nodePackages.markdown-link-check];
in {
default = pkgs.mkShell {
buildInputs =
[
pkgs.pulumi-bin
pkgs.apacheHttpd
pkgs.python
]
++ requirements;
shellHook = ''
export PULUMI_SKIP_UPDATE_CHECK=1
'';
};
ci = pkgs.mkShell {
buildInputs = requirements;
};
};
}
);
}

16
hooks/pre-push Executable file
View File

@@ -0,0 +1,16 @@
#!/usr/bin/python3
import subprocess
def check_fantomas():
result = subprocess.run(["dotnet", "tool", "run", "fantomas", "--check", "-r", "."])
if result.returncode != 0:
raise Exception(f"Formatting incomplete (return code: {result.returncode}). Consider running `dotnet tool run fantomas -r .`")
def main():
check_fantomas()
if __name__ == "__main__":
main()

6
image.nix Normal file
View File

@@ -0,0 +1,6 @@
{pkgs ? import <nixpkgs> {system = "x86_64-linux";}}: let
config = {
imports = [<nixpkgs/nixos/modules/virtualisation/digital-ocean-image.nix>];
};
in
(pkgs.pkgsCross.aarch64-multiplatform-musl.nixos config).digitalOceanImage

694
nix/deps.nix Normal file
View File

@@ -0,0 +1,694 @@
# This file was automatically generated by passthru.fetch-deps.
# Please don't edit it manually, your changes might get overwritten!
{fetchNuGet}: [
(fetchNuGet {
pname = "Ben.Demystifier";
version = "0.4.1";
sha256 = "1szlrhvwpwkjhpgvjlrpjg714bz1yhyljs72pxni3li4mgnklk1f";
})
(fetchNuGet {
pname = "FsCheck";
version = "2.16.5";
sha256 = "0h0k91myra1ricvmlvn27wp3kdr7ib3q55yg5isk6206wh9yhigr";
})
(fetchNuGet {
pname = "FSharp.Core";
version = "7.0.0";
sha256 = "1pgk3qk9p1s53wvja17744x4bf7zs3a3wf0dmxi66w1w06z7i85x";
})
(fetchNuGet {
pname = "FsUnit";
version = "5.0.0";
sha256 = "0r535cw9ikm8xmyla6ah7qx3hb7nvz5m9fi0dqgbkd3wsrc8jlpl";
})
(fetchNuGet {
pname = "Grpc.Core.Api";
version = "2.43.0";
sha256 = "0aywd270inzfy3nizdvz7z1c083m11xfd76016q0c9sbmwrhv20j";
})
(fetchNuGet {
pname = "Grpc.Net.Client";
version = "2.43.0";
sha256 = "1yxm894lpn5sg6xg7i5ldd9bh7xg2s2c6xsx0yf7zrachy1bqbar";
})
(fetchNuGet {
pname = "Grpc.Net.Common";
version = "2.43.0";
sha256 = "17gn73ccqq5aap7r8i6nir1260f5ndav2yc67wk2gnp94nsavbm2";
})
(fetchNuGet {
pname = "Microsoft.CodeCoverage";
version = "17.1.0";
sha256 = "1ijl3w14lnj15hi052jlshf5k8vb90x0py7yrs897mf126qp8ivy";
})
(fetchNuGet {
pname = "Microsoft.CSharp";
version = "4.0.1";
sha256 = "0zxc0apx1gcx361jlq8smc9pfdgmyjh6hpka8dypc9w23nlsh6yj";
})
(fetchNuGet {
pname = "Microsoft.CSharp";
version = "4.3.0";
sha256 = "0gw297dgkh0al1zxvgvncqs0j15lsna9l1wpqas4rflmys440xvb";
})
(fetchNuGet {
pname = "Microsoft.Extensions.DependencyInjection.Abstractions";
version = "2.0.0";
sha256 = "1pwrfh9b72k9rq6mb2jab5qhhi225d5rjalzkapiayggmygc8nhz";
})
(fetchNuGet {
pname = "Microsoft.Extensions.Logging";
version = "2.0.0";
sha256 = "1jkwjcq1ld9znz1haazk8ili2g4pzfdp6i7r7rki4hg3jcadn386";
})
(fetchNuGet {
pname = "Microsoft.Extensions.Logging.Abstractions";
version = "3.1.16";
sha256 = "1cd3wq6j01bczi9d4qq698gindj4bnf5byq25m22741i547ib90i";
})
(fetchNuGet {
pname = "Microsoft.Extensions.Options";
version = "2.0.0";
sha256 = "0g4zadlg73f507krilhaaa7h0jdga216syrzjlyf5fdk25gxmjqh";
})
(fetchNuGet {
pname = "Microsoft.Extensions.Primitives";
version = "2.0.0";
sha256 = "1xppr5jbny04slyjgngxjdm0maxdh47vq481ps944d7jrfs0p3mb";
})
(fetchNuGet {
pname = "Microsoft.NET.Test.Sdk";
version = "17.1.0";
sha256 = "1jaq11fhcfiylnn6wvbp2k9hrgq4cz755sfqjqjqcdxlkiyj2dkw";
})
(fetchNuGet {
pname = "Microsoft.NETCore.Platforms";
version = "1.0.1";
sha256 = "01al6cfxp68dscl15z7rxfw9zvhm64dncsw09a1vmdkacsa2v6lr";
})
(fetchNuGet {
pname = "Microsoft.NETCore.Platforms";
version = "1.1.0";
sha256 = "08vh1r12g6ykjygq5d3vq09zylgb84l63k49jc4v8faw9g93iqqm";
})
(fetchNuGet {
pname = "Microsoft.NETCore.Targets";
version = "1.0.1";
sha256 = "0ppdkwy6s9p7x9jix3v4402wb171cdiibq7js7i13nxpdky7074p";
})
(fetchNuGet {
pname = "Microsoft.NETCore.Targets";
version = "1.1.0";
sha256 = "193xwf33fbm0ni3idxzbr5fdq3i2dlfgihsac9jj7whj0gd902nh";
})
(fetchNuGet {
pname = "Microsoft.TestPlatform.ObjectModel";
version = "17.1.0";
sha256 = "0jw577vbrplv9kga22lsipz91ww9iqi6j1wgpwga0vrayhggjsk2";
})
(fetchNuGet {
pname = "Microsoft.TestPlatform.TestHost";
version = "17.1.0";
sha256 = "0j9i078hv4qqrg2433p20pykmcjvmzarc1cy1k5f7kc7739q6vx5";
})
(fetchNuGet {
pname = "Nager.PublicSuffix";
version = "2.4.0";
sha256 = "0v935sr0100w7j8b6yyi2ssh10az60biq32bvjqkhmgz1370q1ys";
})
(fetchNuGet {
pname = "Namotion.Reflection";
version = "2.1.0";
sha256 = "0ql10m9i5qm3cmcw6abk6wvm823vc4s8wzx351yffd6syd50mkb7";
})
(fetchNuGet {
pname = "NETStandard.Library";
version = "2.0.0";
sha256 = "1bc4ba8ahgk15m8k4nd7x406nhi0kwqzbgjk2dmw52ss553xz7iy";
})
(fetchNuGet {
pname = "NETStandard.Library";
version = "2.0.3";
sha256 = "1fn9fxppfcg4jgypp2pmrpr6awl3qz1xmnri0cygpkwvyx27df1y";
})
(fetchNuGet {
pname = "Newtonsoft.Json";
version = "13.0.1";
sha256 = "0fijg0w6iwap8gvzyjnndds0q4b8anwxxvik7y8vgq97dram4srb";
})
(fetchNuGet {
pname = "Newtonsoft.Json";
version = "9.0.1";
sha256 = "0mcy0i7pnfpqm4pcaiyzzji4g0c8i3a5gjz28rrr28110np8304r";
})
(fetchNuGet {
pname = "NJsonSchema";
version = "10.8.0";
sha256 = "1mzqskv4vx5mzq0rykjwgc323afs2km0hslr2xr6r9fz9qygd28h";
})
(fetchNuGet {
pname = "NuGet.Frameworks";
version = "5.11.0";
sha256 = "0wv26gq39hfqw9md32amr5771s73f5zn1z9vs4y77cgynxr73s4z";
})
(fetchNuGet {
pname = "NUnit";
version = "3.13.3";
sha256 = "0wdzfkygqnr73s6lpxg5b1pwaqz9f414fxpvpdmf72bvh4jaqzv6";
})
(fetchNuGet {
pname = "NUnit.Analyzers";
version = "3.3.0";
sha256 = "00wp5q361f845aywrhhfbrpwd2srgygiam30pvn846b5dbl41vy0";
})
(fetchNuGet {
pname = "NUnit3TestAdapter";
version = "4.2.1";
sha256 = "0gildh4xcb6gkxcrrgh5a1j7lq0a7l670jpbs71akl5b5bgy5gc3";
})
(fetchNuGet {
pname = "OneOf";
version = "3.0.216";
sha256 = "0hhbka3ajxnhbvm39zpzn8bfdyjdx4pg5w2sixs0vkycq5q4ir10";
})
(fetchNuGet {
pname = "Pulumi";
version = "3.37.2";
sha256 = "0szsw6yyvanp69yaa5cysx9m7ixn62djvk8jf68zlgrm2gngk7lr";
})
(fetchNuGet {
pname = "Pulumi.Cloudflare";
version = "4.9.0";
sha256 = "1wqa059wbqzd26jn9v1rjyq70h7ypvcx549l3g1vva4b0nc699dx";
})
(fetchNuGet {
pname = "Pulumi.Command";
version = "0.4.1";
sha256 = "0d6k8y2r0s6d0qjq4h6ixy6sz8lilbfpgbimk9wg8vv8drlbb2h9";
})
(fetchNuGet {
pname = "Pulumi.DigitalOcean";
version = "4.14.0";
sha256 = "17cd86nvhhcqaznz2g386fwj1vdzbqvm84x8wzqp5g1r7rdadbb3";
})
(fetchNuGet {
pname = "Pulumi.Protobuf";
version = "3.20.1";
sha256 = "01qy17p1hayda462h48cdlmnhjf86m1fbjwfy6915jjajwcxb4pz";
})
(fetchNuGet {
pname = "runtime.any.System.Collections";
version = "4.3.0";
sha256 = "0bv5qgm6vr47ynxqbnkc7i797fdi8gbjjxii173syrx14nmrkwg0";
})
(fetchNuGet {
pname = "runtime.any.System.Diagnostics.Tools";
version = "4.3.0";
sha256 = "1wl76vk12zhdh66vmagni66h5xbhgqq7zkdpgw21jhxhvlbcl8pk";
})
(fetchNuGet {
pname = "runtime.any.System.Diagnostics.Tracing";
version = "4.3.0";
sha256 = "00j6nv2xgmd3bi347k00m7wr542wjlig53rmj28pmw7ddcn97jbn";
})
(fetchNuGet {
pname = "runtime.any.System.Globalization";
version = "4.3.0";
sha256 = "1daqf33hssad94lamzg01y49xwndy2q97i2lrb7mgn28656qia1x";
})
(fetchNuGet {
pname = "runtime.any.System.IO";
version = "4.3.0";
sha256 = "0l8xz8zn46w4d10bcn3l4yyn4vhb3lrj2zw8llvz7jk14k4zps5x";
})
(fetchNuGet {
pname = "runtime.any.System.Reflection";
version = "4.3.0";
sha256 = "02c9h3y35pylc0zfq3wcsvc5nqci95nrkq0mszifc0sjx7xrzkly";
})
(fetchNuGet {
pname = "runtime.any.System.Reflection.Extensions";
version = "4.3.0";
sha256 = "0zyri97dfc5vyaz9ba65hjj1zbcrzaffhsdlpxc9bh09wy22fq33";
})
(fetchNuGet {
pname = "runtime.any.System.Reflection.Primitives";
version = "4.3.0";
sha256 = "0x1mm8c6iy8rlxm8w9vqw7gb7s1ljadrn049fmf70cyh42vdfhrf";
})
(fetchNuGet {
pname = "runtime.any.System.Resources.ResourceManager";
version = "4.3.0";
sha256 = "03kickal0iiby82wa5flar18kyv82s9s6d4xhk5h4bi5kfcyfjzl";
})
(fetchNuGet {
pname = "runtime.any.System.Runtime";
version = "4.3.0";
sha256 = "1cqh1sv3h5j7ixyb7axxbdkqx6cxy00p4np4j91kpm492rf4s25b";
})
(fetchNuGet {
pname = "runtime.any.System.Runtime.Handles";
version = "4.3.0";
sha256 = "0bh5bi25nk9w9xi8z23ws45q5yia6k7dg3i4axhfqlnj145l011x";
})
(fetchNuGet {
pname = "runtime.any.System.Runtime.InteropServices";
version = "4.3.0";
sha256 = "0c3g3g3jmhlhw4klrc86ka9fjbl7i59ds1fadsb2l8nqf8z3kb19";
})
(fetchNuGet {
pname = "runtime.any.System.Text.Encoding";
version = "4.3.0";
sha256 = "0aqqi1v4wx51h51mk956y783wzags13wa7mgqyclacmsmpv02ps3";
})
(fetchNuGet {
pname = "runtime.any.System.Text.Encoding.Extensions";
version = "4.3.0";
sha256 = "0lqhgqi0i8194ryqq6v2gqx0fb86db2gqknbm0aq31wb378j7ip8";
})
(fetchNuGet {
pname = "runtime.any.System.Threading.Tasks";
version = "4.3.0";
sha256 = "03mnvkhskbzxddz4hm113zsch1jyzh2cs450dk3rgfjp8crlw1va";
})
(fetchNuGet {
pname = "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "16rnxzpk5dpbbl1x354yrlsbvwylrq456xzpsha1n9y3glnhyx9d";
})
(fetchNuGet {
pname = "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "0hkg03sgm2wyq8nqk6dbm9jh5vcq57ry42lkqdmfklrw89lsmr59";
})
(fetchNuGet {
pname = "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "0c2p354hjx58xhhz7wv6div8xpi90sc6ibdm40qin21bvi7ymcaa";
})
(fetchNuGet {
pname = "runtime.native.System";
version = "4.3.0";
sha256 = "15hgf6zaq9b8br2wi1i3x0zvmk410nlmsmva9p0bbg73v6hml5k4";
})
(fetchNuGet {
pname = "runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "18pzfdlwsg2nb1jjjjzyb5qlgy6xjxzmhnfaijq5s2jw3cm3ab97";
})
(fetchNuGet {
pname = "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "0qyynf9nz5i7pc26cwhgi8j62ps27sqmf78ijcfgzab50z9g8ay3";
})
(fetchNuGet {
pname = "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "1klrs545awhayryma6l7g2pvnp9xy4z0r1i40r80zb45q3i9nbyf";
})
(fetchNuGet {
pname = "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "0zcxjv5pckplvkg0r6mw3asggm7aqzbdjimhvsasb0cgm59x09l3";
})
(fetchNuGet {
pname = "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "0vhynn79ih7hw7cwjazn87rm9z9fj0rvxgzlab36jybgcpcgphsn";
})
(fetchNuGet {
pname = "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "160p68l2c7cqmyqjwxydcvgw7lvl1cr0znkw8fp24d1by9mqc8p3";
})
(fetchNuGet {
pname = "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "15zrc8fgd8zx28hdghcj5f5i34wf3l6bq5177075m2bc2j34jrqy";
})
(fetchNuGet {
pname = "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl";
version = "4.3.0";
sha256 = "1p4dgxax6p7rlgj4q73k73rslcnz4wdcv8q2flg1s8ygwcm58ld5";
})
(fetchNuGet {
pname = "runtime.unix.System.Diagnostics.Debug";
version = "4.3.0";
sha256 = "1lps7fbnw34bnh3lm31gs5c0g0dh7548wfmb8zz62v0zqz71msj5";
})
(fetchNuGet {
pname = "runtime.unix.System.IO.FileSystem";
version = "4.3.0";
sha256 = "14nbkhvs7sji5r1saj2x8daz82rnf9kx28d3v2qss34qbr32dzix";
})
(fetchNuGet {
pname = "runtime.unix.System.Private.Uri";
version = "4.3.0";
sha256 = "1jx02q6kiwlvfksq1q9qr17fj78y5v6mwsszav4qcz9z25d5g6vk";
})
(fetchNuGet {
pname = "runtime.unix.System.Runtime.Extensions";
version = "4.3.0";
sha256 = "0pnxxmm8whx38dp6yvwgmh22smknxmqs5n513fc7m4wxvs1bvi4p";
})
(fetchNuGet {
pname = "Semver";
version = "2.1.0";
sha256 = "02hpqh508s21x9qj6884651jn1s97gkxs54iar47dkxi8b5imq34";
})
(fetchNuGet {
pname = "Serilog";
version = "2.10.0";
sha256 = "08bih205i632ywryn3zxkhb15dwgyaxbhmm1z3b5nmby9fb25k7v";
})
(fetchNuGet {
pname = "Serilog";
version = "2.8.0";
sha256 = "0fnrs05yjnni06mbax7ig74wiiqjyyhrxmr1hrhlpwcmc40zs4ih";
})
(fetchNuGet {
pname = "Serilog.Extensions.Logging";
version = "3.0.1";
sha256 = "069qy7dm5nxb372ij112ppa6m99b4iaimj3sji74m659fwrcrl9a";
})
(fetchNuGet {
pname = "Serilog.Sinks.Console";
version = "4.0.1";
sha256 = "080vh9kcyn9lx4j7p34146kp9byvhqlaz5jn9wzx70ql9cwd0hlz";
})
(fetchNuGet {
pname = "System.Buffers";
version = "4.3.0";
sha256 = "0fgns20ispwrfqll4q1zc1waqcmylb3zc50ys9x8zlwxh9pmd9jy";
})
(fetchNuGet {
pname = "System.Collections";
version = "4.0.11";
sha256 = "1ga40f5lrwldiyw6vy67d0sg7jd7ww6kgwbksm19wrvq9hr0bsm6";
})
(fetchNuGet {
pname = "System.Collections";
version = "4.3.0";
sha256 = "19r4y64dqyrq6k4706dnyhhw7fs24kpp3awak7whzss39dakpxk9";
})
(fetchNuGet {
pname = "System.Collections.NonGeneric";
version = "4.3.0";
sha256 = "07q3k0hf3mrcjzwj8fwk6gv3n51cb513w4mgkfxzm3i37sc9kz7k";
})
(fetchNuGet {
pname = "System.Diagnostics.Debug";
version = "4.0.11";
sha256 = "0gmjghrqmlgzxivd2xl50ncbglb7ljzb66rlx8ws6dv8jm0d5siz";
})
(fetchNuGet {
pname = "System.Diagnostics.Debug";
version = "4.3.0";
sha256 = "00yjlf19wjydyr6cfviaph3vsjzg3d5nvnya26i2fvfg53sknh3y";
})
(fetchNuGet {
pname = "System.Diagnostics.Tools";
version = "4.0.1";
sha256 = "19cknvg07yhakcvpxg3cxa0bwadplin6kyxd8mpjjpwnp56nl85x";
})
(fetchNuGet {
pname = "System.Diagnostics.Tracing";
version = "4.3.0";
sha256 = "1m3bx6c2s958qligl67q7grkwfz3w53hpy7nc97mh6f7j5k168c4";
})
(fetchNuGet {
pname = "System.Dynamic.Runtime";
version = "4.0.11";
sha256 = "1pla2dx8gkidf7xkciig6nifdsb494axjvzvann8g2lp3dbqasm9";
})
(fetchNuGet {
pname = "System.Dynamic.Runtime";
version = "4.3.0";
sha256 = "1d951hrvrpndk7insiag80qxjbf2y0y39y8h5hnq9612ws661glk";
})
(fetchNuGet {
pname = "System.Globalization";
version = "4.0.11";
sha256 = "070c5jbas2v7smm660zaf1gh0489xanjqymkvafcs4f8cdrs1d5d";
})
(fetchNuGet {
pname = "System.Globalization";
version = "4.3.0";
sha256 = "1cp68vv683n6ic2zqh2s1fn4c2sd87g5hpp6l4d4nj4536jz98ki";
})
(fetchNuGet {
pname = "System.IO";
version = "4.1.0";
sha256 = "1g0yb8p11vfd0kbkyzlfsbsp5z44lwsvyc0h3dpw6vqnbi035ajp";
})
(fetchNuGet {
pname = "System.IO";
version = "4.3.0";
sha256 = "05l9qdrzhm4s5dixmx68kxwif4l99ll5gqmh7rqgw554fx0agv5f";
})
(fetchNuGet {
pname = "System.IO.FileSystem";
version = "4.0.1";
sha256 = "0kgfpw6w4djqra3w5crrg8xivbanh1w9dh3qapb28q060wb9flp1";
})
(fetchNuGet {
pname = "System.IO.FileSystem.Primitives";
version = "4.0.1";
sha256 = "1s0mniajj3lvbyf7vfb5shp4ink5yibsx945k6lvxa96r8la1612";
})
(fetchNuGet {
pname = "System.Linq";
version = "4.1.0";
sha256 = "1ppg83svb39hj4hpp5k7kcryzrf3sfnm08vxd5sm2drrijsla2k5";
})
(fetchNuGet {
pname = "System.Linq";
version = "4.3.0";
sha256 = "1w0gmba695rbr80l1k2h4mrwzbzsyfl2z4klmpbsvsg5pm4a56s7";
})
(fetchNuGet {
pname = "System.Linq.Expressions";
version = "4.1.0";
sha256 = "1gpdxl6ip06cnab7n3zlcg6mqp7kknf73s8wjinzi4p0apw82fpg";
})
(fetchNuGet {
pname = "System.Linq.Expressions";
version = "4.3.0";
sha256 = "0ky2nrcvh70rqq88m9a5yqabsl4fyd17bpr63iy2mbivjs2nyypv";
})
(fetchNuGet {
pname = "System.Memory";
version = "4.5.3";
sha256 = "0naqahm3wljxb5a911d37mwjqjdxv9l0b49p5dmfyijvni2ppy8a";
})
(fetchNuGet {
pname = "System.ObjectModel";
version = "4.0.12";
sha256 = "1sybkfi60a4588xn34nd9a58png36i0xr4y4v4kqpg8wlvy5krrj";
})
(fetchNuGet {
pname = "System.ObjectModel";
version = "4.3.0";
sha256 = "191p63zy5rpqx7dnrb3h7prvgixmk168fhvvkkvhlazncf8r3nc2";
})
(fetchNuGet {
pname = "System.Private.Uri";
version = "4.3.0";
sha256 = "04r1lkdnsznin0fj4ya1zikxiqr0h6r6a1ww2dsm60gqhdrf0mvx";
})
(fetchNuGet {
pname = "System.Reflection";
version = "4.1.0";
sha256 = "1js89429pfw79mxvbzp8p3q93il6rdff332hddhzi5wqglc4gml9";
})
(fetchNuGet {
pname = "System.Reflection";
version = "4.3.0";
sha256 = "0xl55k0mw8cd8ra6dxzh974nxif58s3k1rjv1vbd7gjbjr39j11m";
})
(fetchNuGet {
pname = "System.Reflection.Emit";
version = "4.0.1";
sha256 = "0ydqcsvh6smi41gyaakglnv252625hf29f7kywy2c70nhii2ylqp";
})
(fetchNuGet {
pname = "System.Reflection.Emit";
version = "4.3.0";
sha256 = "11f8y3qfysfcrscjpjym9msk7lsfxkk4fmz9qq95kn3jd0769f74";
})
(fetchNuGet {
pname = "System.Reflection.Emit.ILGeneration";
version = "4.0.1";
sha256 = "1pcd2ig6bg144y10w7yxgc9d22r7c7ww7qn1frdfwgxr24j9wvv0";
})
(fetchNuGet {
pname = "System.Reflection.Emit.ILGeneration";
version = "4.3.0";
sha256 = "0w1n67glpv8241vnpz1kl14sy7zlnw414aqwj4hcx5nd86f6994q";
})
(fetchNuGet {
pname = "System.Reflection.Emit.Lightweight";
version = "4.0.1";
sha256 = "1s4b043zdbx9k39lfhvsk68msv1nxbidhkq6nbm27q7sf8xcsnxr";
})
(fetchNuGet {
pname = "System.Reflection.Emit.Lightweight";
version = "4.3.0";
sha256 = "0ql7lcakycrvzgi9kxz1b3lljd990az1x6c4jsiwcacrvimpib5c";
})
(fetchNuGet {
pname = "System.Reflection.Extensions";
version = "4.0.1";
sha256 = "0m7wqwq0zqq9gbpiqvgk3sr92cbrw7cp3xn53xvw7zj6rz6fdirn";
})
(fetchNuGet {
pname = "System.Reflection.Extensions";
version = "4.3.0";
sha256 = "02bly8bdc98gs22lqsfx9xicblszr2yan7v2mmw3g7hy6miq5hwq";
})
(fetchNuGet {
pname = "System.Reflection.Metadata";
version = "1.6.0";
sha256 = "1wdbavrrkajy7qbdblpbpbalbdl48q3h34cchz24gvdgyrlf15r4";
})
(fetchNuGet {
pname = "System.Reflection.Metadata";
version = "5.0.0";
sha256 = "17qsl5nanlqk9iz0l5wijdn6ka632fs1m1fvx18dfgswm258r3ss";
})
(fetchNuGet {
pname = "System.Reflection.Primitives";
version = "4.0.1";
sha256 = "1bangaabhsl4k9fg8khn83wm6yial8ik1sza7401621jc6jrym28";
})
(fetchNuGet {
pname = "System.Reflection.Primitives";
version = "4.3.0";
sha256 = "04xqa33bld78yv5r93a8n76shvc8wwcdgr1qvvjh959g3rc31276";
})
(fetchNuGet {
pname = "System.Reflection.TypeExtensions";
version = "4.1.0";
sha256 = "1bjli8a7sc7jlxqgcagl9nh8axzfl11f4ld3rjqsyxc516iijij7";
})
(fetchNuGet {
pname = "System.Reflection.TypeExtensions";
version = "4.3.0";
sha256 = "0y2ssg08d817p0vdag98vn238gyrrynjdj4181hdg780sif3ykp1";
})
(fetchNuGet {
pname = "System.Resources.ResourceManager";
version = "4.0.1";
sha256 = "0b4i7mncaf8cnai85jv3wnw6hps140cxz8vylv2bik6wyzgvz7bi";
})
(fetchNuGet {
pname = "System.Resources.ResourceManager";
version = "4.3.0";
sha256 = "0sjqlzsryb0mg4y4xzf35xi523s4is4hz9q4qgdvlvgivl7qxn49";
})
(fetchNuGet {
pname = "System.Runtime";
version = "4.1.0";
sha256 = "02hdkgk13rvsd6r9yafbwzss8kr55wnj8d5c7xjnp8gqrwc8sn0m";
})
(fetchNuGet {
pname = "System.Runtime";
version = "4.3.0";
sha256 = "066ixvgbf2c929kgknshcxqj6539ax7b9m570cp8n179cpfkapz7";
})
(fetchNuGet {
pname = "System.Runtime.CompilerServices.Unsafe";
version = "4.4.0";
sha256 = "0a6ahgi5b148sl5qyfpyw383p3cb4yrkm802k29fsi4mxkiwir29";
})
(fetchNuGet {
pname = "System.Runtime.Extensions";
version = "4.1.0";
sha256 = "0rw4rm4vsm3h3szxp9iijc3ksyviwsv6f63dng3vhqyg4vjdkc2z";
})
(fetchNuGet {
pname = "System.Runtime.Extensions";
version = "4.3.0";
sha256 = "1ykp3dnhwvm48nap8q23893hagf665k0kn3cbgsqpwzbijdcgc60";
})
(fetchNuGet {
pname = "System.Runtime.Handles";
version = "4.0.1";
sha256 = "1g0zrdi5508v49pfm3iii2hn6nm00bgvfpjq1zxknfjrxxa20r4g";
})
(fetchNuGet {
pname = "System.Runtime.Handles";
version = "4.3.0";
sha256 = "0sw2gfj2xr7sw9qjn0j3l9yw07x73lcs97p8xfc9w1x9h5g5m7i8";
})
(fetchNuGet {
pname = "System.Runtime.InteropServices";
version = "4.1.0";
sha256 = "01kxqppx3dr3b6b286xafqilv4s2n0gqvfgzfd4z943ga9i81is1";
})
(fetchNuGet {
pname = "System.Runtime.InteropServices";
version = "4.3.0";
sha256 = "00hywrn4g7hva1b2qri2s6rabzwgxnbpw9zfxmz28z09cpwwgh7j";
})
(fetchNuGet {
pname = "System.Runtime.Serialization.Primitives";
version = "4.1.1";
sha256 = "042rfjixknlr6r10vx2pgf56yming8lkjikamg3g4v29ikk78h7k";
})
(fetchNuGet {
pname = "System.Text.Encoding";
version = "4.0.11";
sha256 = "1dyqv0hijg265dwxg6l7aiv74102d6xjiwplh2ar1ly6xfaa4iiw";
})
(fetchNuGet {
pname = "System.Text.Encoding";
version = "4.3.0";
sha256 = "1f04lkir4iladpp51sdgmis9dj4y8v08cka0mbmsy0frc9a4gjqr";
})
(fetchNuGet {
pname = "System.Text.Encoding.Extensions";
version = "4.0.11";
sha256 = "08nsfrpiwsg9x5ml4xyl3zyvjfdi4mvbqf93kjdh11j4fwkznizs";
})
(fetchNuGet {
pname = "System.Text.RegularExpressions";
version = "4.1.0";
sha256 = "1mw7vfkkyd04yn2fbhm38msk7dz2xwvib14ygjsb8dq2lcvr18y7";
})
(fetchNuGet {
pname = "System.Threading";
version = "4.0.11";
sha256 = "19x946h926bzvbsgj28csn46gak2crv2skpwsx80hbgazmkgb1ls";
})
(fetchNuGet {
pname = "System.Threading";
version = "4.3.0";
sha256 = "0rw9wfamvhayp5zh3j7p1yfmx9b5khbf4q50d8k5rk993rskfd34";
})
(fetchNuGet {
pname = "System.Threading.Tasks";
version = "4.0.11";
sha256 = "0nr1r41rak82qfa5m0lhk9mp0k93bvfd7bbd9sdzwx9mb36g28p5";
})
(fetchNuGet {
pname = "System.Threading.Tasks";
version = "4.3.0";
sha256 = "134z3v9abw3a6jsw17xl3f6hqjpak5l682k2vz39spj4kmydg6k7";
})
(fetchNuGet {
pname = "System.Threading.Tasks.Extensions";
version = "4.0.0";
sha256 = "1cb51z062mvc2i8blpzmpn9d9mm4y307xrwi65di8ri18cz5r1zr";
})
(fetchNuGet {
pname = "System.Xml.ReaderWriter";
version = "4.0.11";
sha256 = "0c6ky1jk5ada9m94wcadih98l6k1fvf6vi7vhn1msjixaha419l5";
})
(fetchNuGet {
pname = "System.Xml.XDocument";
version = "4.0.11";
sha256 = "0n4lvpqzy9kc7qy1a4acwwd7b7pnvygv895az5640idl2y9zbz18";
})
]

73
nix/fetchDeps.sh Normal file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
# This file was adapted from
# https://github.com/NixOS/nixpkgs/blob/b981d811453ab84fb3ea593a9b33b960f1ab9147/pkgs/build-support/dotnet/build-dotnet-module/default.nix#L173
set -euo pipefail
export PATH="@binPath@"
for arg in "$@"; do
case "$arg" in
--keep-sources|-k)
keepSources=1
shift
;;
--help|-h)
echo "usage: $0 [--keep-sources] [--help] <output path>"
echo " <output path> The path to write the lockfile to. A temporary file is used if this is not set"
echo " --keep-sources Don't remove temporary directories upon exit, useful for debugging"
echo " --help Show this help message"
exit
;;
esac
done
tmp=$(mktemp -td "@pname@-tmp-XXXXXX")
export tmp
HOME=$tmp/home
exitTrap() {
test -n "${ranTrap-}" && return
ranTrap=1
if test -n "${keepSources-}"; then
echo -e "Path to the source: $tmp/src\nPath to the fake home: $tmp/home"
else
rm -rf "$tmp"
fi
# Since mktemp is used this will be empty if the script didnt succesfully complete
if ! test -s "$depsFile"; then
rm -rf "$depsFile"
fi
}
trap exitTrap EXIT INT TERM
dotnetRestore() {
local -r project="${1-}"
local -r rid="$2"
dotnet restore "${project-}" \
-p:ContinuousIntegrationBuild=true \
-p:Deterministic=true \
--packages "$tmp/nuget_pkgs" \
--runtime "$rid" \
--no-cache \
--force
}
declare -a projectFiles=( @projectFiles@ )
declare -a testProjectFiles=( @testProjectFiles@ )
export DOTNET_NOLOGO=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
depsFile=$(realpath "${1:-$(mktemp -t "@pname@-deps-XXXXXX.nix")}")
mkdir -p "$tmp/nuget_pkgs"
storeSrc="@storeSrc@"
src="$tmp/src"
cp -rT "$storeSrc" "$src"
chmod -R +w "$src"
cd "$src"
echo "Restoring project..."
rids=("@rids@")
for rid in "${rids[@]}"; do
(( ${#projectFiles[@]} == 0 )) && dotnetRestore "" "$rid"
for project in "${projectFiles[@]-}" "${testProjectFiles[@]-}"; do
dotnetRestore "$project" "$rid"
done
done
echo "Successfully restored project"
echo "Writing lockfile..."
echo -e "# This file was automatically generated by passthru.fetch-deps.\n# Please don't edit it manually, your changes might get overwritten!\n" > "$depsFile"
nuget-to-nix "$tmp/nuget_pkgs" "@packages@" >> "$depsFile"
echo "Successfully wrote lockfile to $depsFile"