mirror of
https://github.com/Smaug123/PulumiConfig
synced 2025-10-08 01:58:42 +00:00
Create Pulumi-provisioned web server
This commit is contained in:
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"fantomas": {
|
||||
"version": "5.2.0-alpha-010",
|
||||
"commands": [
|
||||
"fantomas"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
41
.editorconfig
Normal file
41
.editorconfig
Normal 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
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
* eol=auto
|
||||
*.sh eol=lf
|
||||
*.nix eol=lf
|
94
.github/workflows/dotnetcore.yml
vendored
Normal file
94
.github/workflows/dotnetcore.yml
vendored
Normal 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
361
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
22
Pulumi.sln
Normal 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
2
Pulumi.sln.DotSettings
Normal 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>
|
29
PulumiWebServer.Test/PulumiWebServer.Test.fsproj
Normal file
29
PulumiWebServer.Test/PulumiWebServer.Test.fsproj
Normal 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>
|
103
PulumiWebServer.Test/TestConfiguration.fs
Normal file
103
PulumiWebServer.Test/TestConfiguration.fs
Normal 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
|
65
PulumiWebServer.Test/TestJsonSchema.fs
Normal file
65
PulumiWebServer.Test/TestJsonSchema.fs
Normal 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 ())
|
21
PulumiWebServer.Test/Utils.fs
Normal file
21
PulumiWebServer.Test/Utils.fs
Normal 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
|
20
PulumiWebServer.Test/exampleconfig.json
Normal file
20
PulumiWebServer.Test/exampleconfig.json
Normal 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"]
|
||||
}
|
26
PulumiWebServer/BashString.fs
Normal file
26
PulumiWebServer/BashString.fs
Normal 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
|
11
PulumiWebServer/BashString.fsi
Normal file
11
PulumiWebServer/BashString.fsi
Normal 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
|
122
PulumiWebServer/Cloudflare.fs
Normal file
122
PulumiWebServer/Cloudflare.fs
Normal 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
|
93
PulumiWebServer/Command.fs
Normal file
93
PulumiWebServer/Command.fs
Normal 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"""
|
197
PulumiWebServer/Configuration.fs
Normal file
197
PulumiWebServer/Configuration.fs
Normal 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
|
42
PulumiWebServer/DigitalOcean.fs
Normal file
42
PulumiWebServer/DigitalOcean.fs
Normal 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
141
PulumiWebServer/Domain.fs
Normal 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
90
PulumiWebServer/Gitea.fs
Normal 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
|
||||
}
|
34
PulumiWebServer/Htpasswd.fs
Normal file
34
PulumiWebServer/Htpasswd.fs
Normal 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
19
PulumiWebServer/Local.fs
Normal 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
13
PulumiWebServer/Module.fs
Normal 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
78
PulumiWebServer/Nginx.fs
Normal 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
64
PulumiWebServer/Nix/flake.lock
generated
Normal 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
|
||||
}
|
23
PulumiWebServer/Nix/flake.nix
Normal file
23
PulumiWebServer/Nix/flake.nix
Normal 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;
|
||||
};
|
||||
}
|
115
PulumiWebServer/Nix/gitea.nix
Normal file
115
PulumiWebServer/Nix/gitea.nix
Normal 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;};
|
||||
};
|
||||
}
|
39
PulumiWebServer/Nix/nginx.nix
Normal file
39
PulumiWebServer/Nix/nginx.nix
Normal 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";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
32
PulumiWebServer/Nix/radicale.nix
Normal file
32
PulumiWebServer/Nix/radicale.nix
Normal 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}/";
|
||||
};
|
||||
};
|
||||
}
|
34
PulumiWebServer/Nix/userconfig.nix
Normal file
34
PulumiWebServer/Nix/userconfig.nix
Normal 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
171
PulumiWebServer/Program.fs
Normal 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
|
5
PulumiWebServer/Pulumi.dev.yaml
Normal file
5
PulumiWebServer/Pulumi.dev.yaml
Normal 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
67
PulumiWebServer/Pulumi.fs
Normal 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 ()
|
5
PulumiWebServer/Pulumi.staging.yaml
Normal file
5
PulumiWebServer/Pulumi.staging.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
config:
|
||||
cloudflare:apiToken:
|
||||
secure: AAABAK391jNLL3SDyFJBn/mBEdcZ7tUyJhwrRsdrHvckN+GzrBw5CJq4+ftaRRSIZEObTd/3wPFmoxcqgmIsiGAEBjHqLGak
|
||||
digitalocean:token:
|
||||
secure: AAABAIypnl37QdxXkzb8LIQvB26ncgvEjf8NgGx+KNe4rzJACTVCvvkxsf2lWG8Zf9uY2PO6WLk4qjIS6Mgm2SdQkEM1HgL2BYxyK+OGPNKb/ks9Dlw+TnkIZRVILyYlyqE7e5DRvg==
|
4
PulumiWebServer/Pulumi.yaml
Normal file
4
PulumiWebServer/Pulumi.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: PulumiWebServer
|
||||
description: Pulumi configuration for my personal web server
|
||||
runtime:
|
||||
name: dotnet
|
44
PulumiWebServer/PulumiWebServer.fsproj
Normal file
44
PulumiWebServer/PulumiWebServer.fsproj
Normal 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
122
PulumiWebServer/Radicale.fs
Normal 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
176
PulumiWebServer/Server.fs
Normal 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
16
PulumiWebServer/Utils.fs
Normal 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 ()
|
114
PulumiWebServer/config.schema.json
Normal file
114
PulumiWebServer/config.schema.json
Normal 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
16
README.md
Normal 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
35
configure.sh
Executable 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
42
flake.lock
generated
Normal 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
94
flake.nix
Normal 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
16
hooks/pre-push
Executable 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
6
image.nix
Normal 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
694
nix/deps.nix
Normal 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
73
nix/fetchDeps.sh
Normal 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"
|
Reference in New Issue
Block a user