mirror of
https://github.com/Smaug123/PulumiConfig
synced 2025-10-05 16:48:39 +00:00
Use Pulumi to provision and Nix to configure (#12)
This commit is contained in:
@@ -2,7 +2,6 @@ root=true
|
||||
|
||||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=crlf
|
||||
trim_trailing_whitespace=true
|
||||
insert_final_newline=true
|
||||
indent_style=space
|
||||
|
9
.sops.yaml
Normal file
9
.sops.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
keys:
|
||||
- &patrick "age1uswp3m453z9vuvqcxcu5a7pnyu7l3vc09q6j99jywc08kag2r30qxk6254"
|
||||
- &staging_server 'age1rg6ngrc38wj8239al0v737lgfgyf6s8rse02jk3z4cjqzhx0g5jq4xv784'
|
||||
creation_rules:
|
||||
- path_regex: "secrets/[^/]+\\.json$"
|
||||
key_groups:
|
||||
- age:
|
||||
- *staging_server
|
||||
- *patrick
|
@@ -9,7 +9,7 @@
|
||||
<Compile Include="Utils.fs" />
|
||||
<Compile Include="TestConfiguration.fs" />
|
||||
<Compile Include="TestJsonSchema.fs" />
|
||||
<EmbeddedResource Include="exampleconfig.json" />
|
||||
<EmbeddedResource Include="..\PulumiWebServer\Nix\config.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -22,30 +22,6 @@ module TestConfiguration =
|
||||
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
|
||||
@@ -59,12 +35,6 @@ module TestConfiguration =
|
||||
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
|
||||
@@ -78,26 +48,18 @@ module TestConfiguration =
|
||||
|
||||
[<Test>]
|
||||
let ``Specific example`` () =
|
||||
let config =
|
||||
let publicConfig =
|
||||
{
|
||||
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
|
||||
}
|
||||
AcmeEmail = EmailAddress "test@example.com"
|
||||
RemoteUsername = Username "non-root"
|
||||
}
|
||||
|
||||
let serialised = SerialisedConfig.Make config
|
||||
let serialised = SerialisedConfig.Make publicConfig
|
||||
let roundTripped = SerialisedConfig.Deserialise serialised
|
||||
config |> shouldEqual roundTripped
|
||||
publicConfig |> shouldEqual roundTripped
|
||||
|
@@ -23,8 +23,7 @@ module TestSchema =
|
||||
|
||||
let schema = JsonSchema.FromJsonAsync(File.ReadAllText schemaFile.FullName).Result
|
||||
|
||||
let json =
|
||||
Utils.getEmbeddedResource typeof<Utils.Dummy>.Assembly "exampleconfig.json"
|
||||
let json = Utils.getEmbeddedResource typeof<Utils.Dummy>.Assembly "config.json"
|
||||
|
||||
let validator = JsonSchemaValidator ()
|
||||
let errors = validator.Validate (json, schema)
|
||||
@@ -33,8 +32,7 @@ module TestSchema =
|
||||
|
||||
[<Test>]
|
||||
let ``Example can be loaded`` () =
|
||||
let config =
|
||||
Utils.getEmbeddedResource typeof<Utils.Dummy>.Assembly "exampleconfig.json"
|
||||
let config = Utils.getEmbeddedResource typeof<Utils.Dummy>.Assembly "config.json"
|
||||
|
||||
use stream = new MemoryStream ()
|
||||
|
||||
|
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
@@ -7,35 +7,10 @@ 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.Host <- address.Get () |> Input.lift
|
||||
|
||||
inputArgs.Port <- Input.lift 22
|
||||
inputArgs.User <- Input.lift "root"
|
||||
@@ -44,50 +19,22 @@ umask "$OLD_UMASK"
|
||||
|
||||
inputArgs |> Output.CreateSecret |> Input.ofOutput
|
||||
|
||||
let contentAddressedCopy
|
||||
let pullFile
|
||||
(PrivateKey privateKey)
|
||||
(address : Address)
|
||||
(name : string)
|
||||
(trigger : Output<'a>)
|
||||
(targetPath : string)
|
||||
(fileContents : string)
|
||||
: Command
|
||||
(commandName : string)
|
||||
(remotePath : BashString)
|
||||
(destPath : BashString)
|
||||
: Pulumi.Command.Local.Command
|
||||
=
|
||||
let args = CommandArgs ()
|
||||
args.Connection <- connection privateKey address
|
||||
let args = Pulumi.Command.Local.CommandArgs ()
|
||||
|
||||
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 argsString =
|
||||
$"scp -i {privateKey.FullName} root@{address.Get ()}:{remotePath} {destPath}"
|
||||
|
||||
let delimiter = "EOF"
|
||||
args.Create <- Input.ofOutput (Output.CreateSecret argsString)
|
||||
|
||||
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"""
|
||||
Pulumi.Command.Local.Command (commandName, args)
|
||||
|
@@ -16,8 +16,6 @@ type Configuration =
|
||||
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
|
||||
@@ -28,17 +26,10 @@ type Configuration =
|
||||
/// world where `Www` were implemented as a subdomain
|
||||
/// and not a cname
|
||||
Subdomains : Set<WellKnownSubdomain>
|
||||
/// Linux user to create on the server
|
||||
/// Email address to use with ACME registration
|
||||
AcmeEmail : EmailAddress
|
||||
/// Username for the user account to be created 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 =
|
||||
@@ -48,62 +39,6 @@ type Configuration =
|
||||
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 =
|
||||
@@ -117,17 +52,15 @@ type SerialisedConfig =
|
||||
[<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)>]
|
||||
AcmeEmail : string
|
||||
[<JsonProperty(Required = Required.Always)>]
|
||||
RemoteUsername : string
|
||||
GiteaConfig : Nullable<SerialisedGiteaConfig>
|
||||
RadicaleConfig : Nullable<SerialisedRadicaleConfig>
|
||||
}
|
||||
|
||||
static member Make (config : Configuration) =
|
||||
@@ -138,7 +71,6 @@ type SerialisedConfig =
|
||||
match config.PublicKeyOverride with
|
||||
| None -> null
|
||||
| Some (PublicKey p) -> p.FullName
|
||||
AcmeEmail = config.AcmeEmail.ToString ()
|
||||
Domain = config.Domain.ToString ()
|
||||
Cnames =
|
||||
config.Cnames
|
||||
@@ -148,12 +80,8 @@ type SerialisedConfig =
|
||||
)
|
||||
|> Dictionary
|
||||
Subdomains = config.Subdomains |> Seq.map (fun sub -> sub.ToString ()) |> Seq.toArray
|
||||
AcmeEmail = config.AcmeEmail.ToString ()
|
||||
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 =
|
||||
@@ -164,7 +92,6 @@ type SerialisedConfig =
|
||||
match config.PublicKey with
|
||||
| null -> None
|
||||
| key -> FileInfo key |> PublicKey |> Some
|
||||
AcmeEmail = config.AcmeEmail |> EmailAddress
|
||||
Domain = config.Domain |> DomainName
|
||||
Cnames =
|
||||
config.Cnames
|
||||
@@ -176,15 +103,8 @@ type SerialisedConfig =
|
||||
match config.Subdomains with
|
||||
| null -> Set.empty
|
||||
| subdomains -> subdomains |> Seq.map WellKnownSubdomain.Parse |> Set.ofSeq
|
||||
AcmeEmail = config.AcmeEmail |> EmailAddress
|
||||
RemoteUsername = config.RemoteUsername |> Username
|
||||
GiteaConfig =
|
||||
config.GiteaConfig
|
||||
|> Option.ofNullable
|
||||
|> Option.map SerialisedGiteaConfig.Deserialise
|
||||
RadicaleConfig =
|
||||
config.RadicaleConfig
|
||||
|> Option.ofNullable
|
||||
|> Option.map SerialisedRadicaleConfig.Deserialise
|
||||
}
|
||||
|
||||
[<RequireQualifiedAccess>]
|
||||
|
@@ -1,90 +0,0 @@
|
||||
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
|
||||
}
|
@@ -1,34 +0,0 @@
|
||||
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}"
|
@@ -1,19 +1,12 @@
|
||||
namespace PulumiWebServer
|
||||
|
||||
open System.Diagnostics
|
||||
open Pulumi
|
||||
open Pulumi.Command.Local
|
||||
|
||||
[<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}"
|
||||
let forgetKey (address : string) : Command =
|
||||
let args = CommandArgs ()
|
||||
args.Create <- Input.lift $"/usr/bin/ssh-keygen -R {address} || exit 0"
|
||||
|
||||
Command ($"forget-key-{address}", args)
|
||||
|
@@ -1,13 +0,0 @@
|
||||
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
|
||||
}
|
@@ -1,78 +1,12 @@
|
||||
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
|
||||
}
|
||||
|
9
PulumiWebServer/Nix/config.json
Normal file
9
PulumiWebServer/Nix/config.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "servername",
|
||||
"privateKey": "/path/to/.ssh/id_ed25519",
|
||||
"domain": "my.domain.example.com",
|
||||
"cnames": {"www": "root"},
|
||||
"subdomains": ["gitea", "calendar"],
|
||||
"acmeEmail": "me@example.com",
|
||||
"remoteUsername": "myself"
|
||||
}
|
36
PulumiWebServer/Nix/configuration.nix
Normal file
36
PulumiWebServer/Nix/configuration.nix
Normal file
@@ -0,0 +1,36 @@
|
||||
{nixpkgs, ...}: let
|
||||
lib = nixpkgs.lib;
|
||||
userConfig = lib.importJSON ./config.json;
|
||||
sshKeys = lib.importJSON ./ssh-keys.json;
|
||||
in {
|
||||
imports = [
|
||||
./sops.nix
|
||||
./radicale-config.nix
|
||||
./gitea-config.nix
|
||||
./userconfig.nix
|
||||
./nginx-config.nix
|
||||
# generated at runtime by nixos-infect and copied here
|
||||
./hardware-configuration.nix
|
||||
./networking.nix
|
||||
];
|
||||
|
||||
services.radicale-config.domain = userConfig.domain;
|
||||
services.radicale-config.subdomain = "calendar";
|
||||
services.radicale-config.enableGit = true;
|
||||
services.userconfig.user = userConfig.remoteUsername;
|
||||
services.userconfig.sshKeys = sshKeys;
|
||||
services.nginx-config.domain = userConfig.domain;
|
||||
services.nginx-config.email = userConfig.acmeEmail;
|
||||
services.nginx-config.webrootSubdomain = "www";
|
||||
services.nginx-config.staging = true;
|
||||
services.gitea-config.subdomain = "gitea";
|
||||
services.gitea-config.domain = userConfig.domain;
|
||||
|
||||
system.stateVersion = "23.05";
|
||||
|
||||
boot.cleanTmpDir = true;
|
||||
zramSwap.enable = true;
|
||||
networking.hostName = userConfig.name;
|
||||
services.openssh.enable = true;
|
||||
users.users.root.openssh.authorizedKeys.keys = sshKeys;
|
||||
}
|
54
PulumiWebServer/Nix/flake.lock
generated
54
PulumiWebServer/Nix/flake.lock
generated
@@ -37,10 +37,62 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1674352297,
|
||||
"narHash": "sha256-OkAnJPrauEcUCrst4/3DKoQfUn2gXKuU6CFvhtMrLgg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "918b760070bb8f48cb511300fcd7e02e13058a2e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "release-22.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1674236650,
|
||||
"narHash": "sha256-B4GKL1YdJnII6DQNNJ4wDW1ySJVx2suB1h/v4Ql8J0Q=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "cfb43ad7b941d9c3606fb35d91228da7ebddbfc5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"sops": "sops"
|
||||
}
|
||||
},
|
||||
"sops": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1674546403,
|
||||
"narHash": "sha256-vkyNv0xzXuEnu9v52TUtRugNmQWIti8c2RhYnbLG71w=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "b6ab3c61e2ca5e07d1f4eb1b67304e2670ea230c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
|
@@ -5,17 +5,20 @@
|
||||
url = "github:nix-community/home-manager";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
sops.url = "github:Mic92/sops-nix";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
self,
|
||||
nixpkgs,
|
||||
sops,
|
||||
home-manager,
|
||||
}: {
|
||||
nixosConfigurations.nixos-server = nixpkgs.lib.nixosSystem {
|
||||
} @ inputs: {
|
||||
nixosConfigurations.default = nixpkgs.lib.nixosSystem {
|
||||
system = "x86_64-linux";
|
||||
modules = [
|
||||
./configuration.nix
|
||||
(import ./configuration.nix (inputs // {inherit inputs;}))
|
||||
sops.nixosModules.sops
|
||||
];
|
||||
};
|
||||
nix.registry.nixpkgs.flake = nixpkgs;
|
||||
|
105
PulumiWebServer/Nix/gitea-config.nix
Normal file
105
PulumiWebServer/Nix/gitea-config.nix
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: {
|
||||
options = {
|
||||
services.gitea-config = {
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "example.com";
|
||||
description = lib.mdDoc "Top-level domain to configure";
|
||||
};
|
||||
subdomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "gitea";
|
||||
description = lib.mdDoc "Subdomain in which to put Gitea";
|
||||
};
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
description = lib.mdDoc "Gitea localhost port";
|
||||
default = 3001;
|
||||
};
|
||||
};
|
||||
};
|
||||
config = {
|
||||
users.users."gitea".extraGroups = [config.users.groups.keys.name];
|
||||
services.gitea = {
|
||||
enable = true;
|
||||
appName = "Gitea";
|
||||
lfs.enable = true;
|
||||
stateDir = "/preserve/gitea/data";
|
||||
database = {
|
||||
type = "postgres";
|
||||
passwordFile = "/run/secrets/gitea_server_password";
|
||||
};
|
||||
domain = "${config.services.gitea-config.subdomain}.${config.services.gitea-config.domain}";
|
||||
rootUrl = "https://${config.services.gitea-config.subdomain}.${config.services.gitea-config.domain}/";
|
||||
httpPort = config.services.gitea-config.port;
|
||||
settings = let
|
||||
docutils = pkgs.python37.withPackages (ps:
|
||||
with ps; [
|
||||
docutils
|
||||
pygments
|
||||
]);
|
||||
in {
|
||||
mailer = {
|
||||
ENABLED = true;
|
||||
FROM = "gitea@" + config.services.gitea-config.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."${config.services.gitea-config.subdomain}.${config.services.gitea-config.domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://localhost:${toString config.services.gitea-config.port}/";
|
||||
};
|
||||
};
|
||||
|
||||
# The Gitea module does not allow adding users declaratively
|
||||
systemd.services.gitea-add-user = {
|
||||
description = "gitea-add-user";
|
||||
wantedBy = ["multi-user.target"];
|
||||
path = [pkgs.gitea];
|
||||
script = builtins.readFile ./gitea/add-user.sh;
|
||||
serviceConfig = {
|
||||
Restart = "no";
|
||||
Type = "oneshot";
|
||||
User = "gitea";
|
||||
Group = "gitea";
|
||||
WorkingDirectory = config.services.gitea.stateDir;
|
||||
SupplementaryGroups = [config.users.groups.keys.name];
|
||||
};
|
||||
environment = {
|
||||
GITEA_WORK_DIR = config.services.gitea.stateDir;
|
||||
GITEA = "${pkgs.gitea}/bin/gitea";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
{
|
||||
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;};
|
||||
};
|
||||
}
|
25
PulumiWebServer/Nix/gitea/add-user.sh
Normal file
25
PulumiWebServer/Nix/gitea/add-user.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
TMPFILE=$(mktemp)
|
||||
PASSWORD=$(cat /run/secrets/gitea_admin_password)
|
||||
GITEA_ADMIN_USERNAME=$(cat /run/secrets/gitea_admin_username)
|
||||
GITEA_ADMIN_EMAIL=$(cat /run/secrets/gitea_admin_email)
|
||||
set +e
|
||||
while [ ! -e /preserve/gitea/data/custom/conf/app.ini ]; do
|
||||
sleep 5
|
||||
done
|
||||
$GITEA migrate -c /preserve/gitea/data/custom/conf/app.ini
|
||||
$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
|
10
PulumiWebServer/Nix/hardware-configuration.nix
Normal file
10
PulumiWebServer/Nix/hardware-configuration.nix
Normal file
@@ -0,0 +1,10 @@
|
||||
{modulesPath, ...}: {
|
||||
imports = [(modulesPath + "/profiles/qemu-guest.nix")];
|
||||
boot.loader.grub.device = "/dev/vda";
|
||||
boot.initrd.availableKernelModules = ["ata_piix" "uhci_hcd" "xen_blkfront"];
|
||||
boot.initrd.kernelModules = ["nvme"];
|
||||
fileSystems."/" = {
|
||||
device = "/dev/vda1";
|
||||
fsType = "ext4";
|
||||
};
|
||||
}
|
21
PulumiWebServer/Nix/networking.nix
Normal file
21
PulumiWebServer/Nix/networking.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{lib, ...}: {
|
||||
# This file was populated at runtime with the networking
|
||||
# details gathered from the active system.
|
||||
networking = {
|
||||
nameservers = [];
|
||||
defaultGateway = "";
|
||||
defaultGateway6 = "";
|
||||
dhcpcd.enable = false;
|
||||
usePredictableInterfaceNames = lib.mkForce false;
|
||||
interfaces = {
|
||||
eth0 = {
|
||||
ipv4.addresses = [];
|
||||
ipv6.addresses = [];
|
||||
ipv4.routes = [];
|
||||
ipv6.routes = [];
|
||||
};
|
||||
};
|
||||
};
|
||||
services.udev.extraRules = ''
|
||||
'';
|
||||
}
|
78
PulumiWebServer/Nix/nginx-config.nix
Normal file
78
PulumiWebServer/Nix/nginx-config.nix
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
options = {
|
||||
services.nginx-config = {
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "example.com";
|
||||
description = lib.mdDoc "Domain to configure";
|
||||
};
|
||||
webrootSubdomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "www";
|
||||
description = lib.mdDoc "Global redirect";
|
||||
};
|
||||
email = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "admin@example.com";
|
||||
description = lib.mdDoc "Email address to use when registering with Let's Encrypt";
|
||||
};
|
||||
staging = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = "true";
|
||||
description = lib.mdDoc "Whether to use the staging Let's Encrypt instance";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
security.acme.acceptTerms = true;
|
||||
security.acme.defaults.email = config.services.nginx-config.email;
|
||||
security.acme.certs = {
|
||||
"${config.services.nginx-config.domain}" = {
|
||||
server =
|
||||
if config.services.nginx-config.staging
|
||||
then "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
else "https://acme-v02.api.letsencrypt.org/directory";
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80 # required for the ACME challenge
|
||||
443
|
||||
];
|
||||
|
||||
users.users."nginx".extraGroups = [config.users.groups.keys.name];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedTlsSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedGzipSettings = true;
|
||||
|
||||
virtualHosts."${config.services.nginx-config.domain}" = {
|
||||
globalRedirect = "${config.services.nginx-config.webrootSubdomain}.${config.services.nginx-config.domain}";
|
||||
addSSL = true;
|
||||
enableACME = true;
|
||||
root = "/preserve/www/html";
|
||||
};
|
||||
|
||||
virtualHosts."${config.services.nginx-config.webrootSubdomain}.${config.services.nginx-config.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";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
{...}: 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";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
67
PulumiWebServer/Nix/radicale-config.nix
Normal file
67
PulumiWebServer/Nix/radicale-config.nix
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
options = {
|
||||
services.radicale-config = {
|
||||
domain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "example.com";
|
||||
description = lib.mdDoc "Top-level domain to configure";
|
||||
};
|
||||
subdomain = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
example = "calendar";
|
||||
description = lib.mdDoc "Subdomain in which to put Radicale";
|
||||
};
|
||||
enableGit = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = lib.mdDoc "Whether to automatically commit calendar updates to a Git repo";
|
||||
};
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
description = lib.mdDoc "Radicale localhost port";
|
||||
default = 5232;
|
||||
};
|
||||
};
|
||||
};
|
||||
config = let
|
||||
filesystem_folder = "/preserve/radicale/data";
|
||||
in {
|
||||
services.radicale = {
|
||||
enable = true;
|
||||
settings = {
|
||||
logging = {
|
||||
level = "debug";
|
||||
};
|
||||
server.hosts = ["0.0.0.0:${toString config.services.radicale-config.port}"];
|
||||
auth = {
|
||||
type = "htpasswd";
|
||||
htpasswd_filename = "/run/secrets/radicale_htcrypt_password";
|
||||
htpasswd_encryption = "bcrypt";
|
||||
};
|
||||
storage =
|
||||
if config.services.radicale-config.enableGit
|
||||
then {
|
||||
filesystem_folder = filesystem_folder;
|
||||
hook = "GIT=${pkgs.git}/bin/git GITIGNORE=${./radicale/.gitignore} /bin/sh ${./radicale/githook.sh}";
|
||||
}
|
||||
else {};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.radicale.serviceConfig.ReadWritePaths = [filesystem_folder];
|
||||
|
||||
systemd.tmpfiles.rules = ["d ${filesystem_folder} 0750 radicale radicale -"];
|
||||
|
||||
services.nginx.virtualHosts."${config.services.radicale-config.subdomain}.${config.services.radicale-config.domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/" = {
|
||||
proxyPass = "http://localhost:${toString config.services.radicale-config.port}/";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
@@ -1,32 +0,0 @@
|
||||
{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}/";
|
||||
};
|
||||
};
|
||||
}
|
3
PulumiWebServer/Nix/radicale/.gitignore
vendored
Normal file
3
PulumiWebServer/Nix/radicale/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.Radicale.cache
|
||||
.Radicale.lock
|
||||
.Radicale.tmp-*
|
19
PulumiWebServer/Nix/radicale/githook.sh
Normal file
19
PulumiWebServer/Nix/radicale/githook.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ -z "$GIT" ]; then
|
||||
echo "Need to call with Git" 1>&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [ ! -d ".git" ]; then
|
||||
"$GIT" init || exit 3
|
||||
"$GIT" config --local core.includesFile "$GITIGNORE" || exit 4
|
||||
GIT_AUTHOR_NAME=$(cat /run/secrets/radicale_user)
|
||||
"$GIT" config --local user.name "$GIT_AUTHOR_NAME" || exit 5
|
||||
GIT_AUTHOR_EMAIL=$(cat /run/secrets/radicale_git_email)
|
||||
"$GIT" config --local user.email "$GIT_AUTHOR_EMAIL" || exit 6
|
||||
fi
|
||||
"$GIT" add -A || exit 7
|
||||
if ! "$GIT" diff --cached --quiet; then
|
||||
"$GIT" commit -m "Changes by $RADICALE_USER" || exit 8
|
||||
fi
|
32
PulumiWebServer/Nix/secrets/staging.json
Normal file
32
PulumiWebServer/Nix/secrets/staging.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"gitea_server_password": "ENC[AES256_GCM,data:VrNOQ1r0wiNcwLmXpwGrV7RJ78w=,iv:QcuYopwg1sZ64jE1LtJDMJxfGzalmXgoBkbc6ppffTU=,tag:pBHdjhK++ssUi6U1LC7/Mw==,type:str]",
|
||||
"gitea_admin_password": "ENC[AES256_GCM,data:M/JZ0x5ca9KAyE+HnbvAohpgQuk=,iv:PZWQ7IJRvRoAOrCJHx9yZaPmM1eEWl21kKTl776Cm4I=,tag:GNqB6vIv5B0ThiNvw/835g==,type:str]",
|
||||
"gitea_admin_username": "ENC[AES256_GCM,data:vYwbK0WnDfc7Ox5YZQ==,iv:VTifWcYPYvkR+9u91f5lovOTVe8jhfDpPCvMQMSjHg0=,tag:IxMny/5HMlpU8tyQJxJHJw==,type:str]",
|
||||
"gitea_admin_email": "ENC[AES256_GCM,data:d/uXN59unzpO7O54lN5qVoyZkMHSIX4iMejWeA4pdIzoiJiWg07mHLmrMhQPSg==,iv:mzg8ZvYAGoMcPI5lDEJ4VFoShoACecZMo4sOAqkKTJ0=,tag:G94XwFlVenBmWx1DD8z1dw==,type:str]",
|
||||
"acme_email": "ENC[AES256_GCM,data:5/Ex62y0nHATgHJMDDBqVtET/t7fwwlWtWVvgzmblCaG,iv:XKe5eXLOSnoL1LedJc/5egOTtFB3JRZCz30BFWLxt3A=,tag:D0IkAOV0VDmhSU++WVlXoA==,type:str]",
|
||||
"radicale_user": "ENC[AES256_GCM,data:aKoxSeTypg==,iv:/r3U99EwAIigJUjISKnEtnFyZYYITEJ45jp4Q3mM0qM=,tag:T9GVZBSUwWUTFK8yS5i6iQ==,type:str]",
|
||||
"radicale_password": "ENC[AES256_GCM,data:ByLueujmUMAM1Edh0YDeNVZ7GMg=,iv:x+JbD1g+NFk2AldmgyFjIbj1CmT+GGFSnx6hhx8ggoE=,tag:i4rnsGv95hC5PjXQzi6k1Q==,type:str]",
|
||||
"radicale_htcrypt_password": "ENC[AES256_GCM,data:vHHIiPjUjM4cQvv9acz8tFmtSdd8/knL0kZaL+6LbNCpzzR/UTZoAqsYtJuQtI8cWAN3tID0MnfM4cNjBwNmV/BBVk8=,iv:56z1d2E/WQ0UP0wyHvaI5YKZoY+90f5AyyxVUhHKEWs=,tag:jiNKYovn37rfG2YHoB4J0g==,type:str]",
|
||||
"radicale_git_email": "ENC[AES256_GCM,data:xBjo3aIPEH3WIg8qBfMrQ1VXeEkUZ5Ynl0dEWeYGirqL/Y9QOA==,iv:bMi2QCvCnhfQT7+jTXb9PzVPDVr9DPiaEVmVMTRVGZI=,tag:V7Bmp/KGKpaH1ldHwF3/jA==,type:str]",
|
||||
"sops": {
|
||||
"kms": null,
|
||||
"gcp_kms": null,
|
||||
"azure_kv": null,
|
||||
"hc_vault": null,
|
||||
"age": [
|
||||
{
|
||||
"recipient": "age1rg6ngrc38wj8239al0v737lgfgyf6s8rse02jk3z4cjqzhx0g5jq4xv784",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCaXlYaU1nb05oTjdzQTRV\nY1FpL2RJdHdGWm96M1Bvckxuc1dxUUkzS0dFClk3MTNvL1l1S3J1bEdxS2NhMUcz\ndHpDZkZiaUUwellOYnhpaWVXMW5qeEUKLS0tIElkaVY4L2NrblNjVEY5cWxLcndB\nemUwcGJkN0lLeEtjSEMvRFhKRnlsaE0K3PHJDhc24U3YYtSw972xd+jZtCdp4UWL\ngN2ZseqmdN+fLapND8MD+cthHbm320d/MNXvtsed6UjZt2/m1cO0FA==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
},
|
||||
{
|
||||
"recipient": "age1uswp3m453z9vuvqcxcu5a7pnyu7l3vc09q6j99jywc08kag2r30qxk6254",
|
||||
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSTTFSSWErV2c5U3hGTXE2\nNUVBTWFZYUZzTC9TbDN3TnlUV1pDRnVKbjBnCnFxeHF4UkwwZWRNeGsycWowOS9X\nblpRTytRME92YmI4WWlmZlhMV1NzOVkKLS0tIDVMQlh0QmtrZmYzUGd2ait4aGZK\nMFhON2V4WnhBWk1keWhPRTI2UEFIZmsKgG7U1/PNytYja8FnsmDVz7Xi5C2TjRkN\nJctlm3x1yZGoaneSTcwxjrhar3pWt+wEqklPFaeyEzzj0OxEfeIoRg==\n-----END AGE ENCRYPTED FILE-----\n"
|
||||
}
|
||||
],
|
||||
"lastmodified": "2023-02-01T21:53:59Z",
|
||||
"mac": "ENC[AES256_GCM,data:wVm9LcMJ4gT3PpwyagSd386o6JGa5pUvetFwTkad9lyXQQ5k7pGKoVevvvWK9T4/UqOnjXuIA9IYCFC5OL5/jDmawOWCW028bMknArBHZVYXaY2SkBcU/iCdEJd8ox05Lv1KQRYfFNS828q6ghCT9tWIHk8xGO2WoqPPKB5G+rc=,iv:JnP/YF3EDm31hiN/YSq7dtgcqm7dA6n4VaiwvwfEGnw=,tag:wyLMu2QtXRTYsr4n5mlAkw==,type:str]",
|
||||
"pgp": null,
|
||||
"unencrypted_suffix": "_unencrypted",
|
||||
"version": "3.7.3"
|
||||
}
|
||||
}
|
17
PulumiWebServer/Nix/sops.nix
Normal file
17
PulumiWebServer/Nix/sops.nix
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
config,
|
||||
sops,
|
||||
...
|
||||
}: {
|
||||
sops.defaultSopsFile = ./secrets/staging.json;
|
||||
sops.secrets = {
|
||||
"gitea_server_password" = {owner = "gitea";};
|
||||
"gitea_admin_password" = {owner = "gitea";};
|
||||
"gitea_admin_username" = {owner = "gitea";};
|
||||
"gitea_admin_email" = {owner = "gitea";};
|
||||
"radicale_user" = {owner = "radicale";};
|
||||
"radicale_htcrypt_password" = {owner = "radicale";};
|
||||
"radicale_password" = {owner = "radicale";};
|
||||
"radicale_git_email" = {owner = "radicale";};
|
||||
};
|
||||
}
|
2
PulumiWebServer/Nix/ssh-keys.json
Normal file
2
PulumiWebServer/Nix/ssh-keys.json
Normal file
@@ -0,0 +1,2 @@
|
||||
[
|
||||
]
|
@@ -1,17 +1,37 @@
|
||||
{pkgs, ...}: {
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
options = {
|
||||
services.userconfig = {
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = lib.mdDoc "Primary user to create";
|
||||
};
|
||||
sshKeys = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = lib.mdDoc "SSH public keys to register as authorised login methods for this user";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
users.mutableUsers = false;
|
||||
users.users."@@USER@@" = {
|
||||
|
||||
users.users."${config.services.userconfig.user}" = {
|
||||
isNormalUser = true;
|
||||
home = "/home/@@USER@@";
|
||||
home = "/home/${config.services.userconfig.user}";
|
||||
extraGroups = ["wheel"];
|
||||
openssh.authorizedKeys.keys = ["@@AUTHORIZED_KEYS@@"];
|
||||
openssh.authorizedKeys.keys = config.services.userconfig.sshKeys;
|
||||
};
|
||||
|
||||
security.sudo = {
|
||||
enable = true;
|
||||
extraRules = [
|
||||
{
|
||||
users = ["@@USER@@"];
|
||||
users = ["${config.services.userconfig.user}"];
|
||||
commands = [
|
||||
{
|
||||
command = "ALL";
|
||||
@@ -31,4 +51,5 @@
|
||||
pkgs.git
|
||||
pkgs.home-manager
|
||||
];
|
||||
};
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
open System
|
||||
open Nager.PublicSuffix
|
||||
open Newtonsoft.Json
|
||||
open Pulumi
|
||||
open Pulumi.DigitalOcean
|
||||
open System.IO
|
||||
@@ -15,7 +16,7 @@ module Program =
|
||||
|
||||
let config =
|
||||
use file =
|
||||
FileInfo("/Users/patrick/Documents/GitHub/WebsiteConfig/config.json")
|
||||
FileInfo("/Users/patrick/Documents/GitHub/Pulumi/PulumiWebServer/Nix/config.json")
|
||||
.OpenRead ()
|
||||
|
||||
Configuration.get file
|
||||
@@ -56,7 +57,7 @@ module Program =
|
||||
let! droplet =
|
||||
keys
|
||||
|> Array.map (SshKey.fingerprint >> Input.lift)
|
||||
|> DigitalOcean.makeNixosServer "server-staging" Region.LON1
|
||||
|> DigitalOcean.makeNixosServer config.Name Region.LON1
|
||||
|
||||
let! ipv4 = droplet.Ipv4Address
|
||||
let! ipv6 = droplet.Ipv6Address
|
||||
@@ -72,7 +73,8 @@ module Program =
|
||||
let dns =
|
||||
Cloudflare.addDns config.Domain config.Cnames config.Subdomains zone address
|
||||
|
||||
let! _ = Server.waitForReady config.PrivateKey address
|
||||
let readyCommand = Server.waitForReady 1 config.PrivateKey address
|
||||
let! _ = readyCommand.Stdout
|
||||
|
||||
let deps =
|
||||
let dnsDeps =
|
||||
@@ -93,77 +95,53 @@ module Program =
|
||||
let infectNix = Server.infectNix config.PrivateKey address
|
||||
let! _ = infectNix.Stdout
|
||||
|
||||
// Pull the configuration files down.
|
||||
let pullNetworking =
|
||||
Command.pullFile
|
||||
config.PrivateKey
|
||||
address
|
||||
infectNix.Stdout
|
||||
"pull-networking"
|
||||
(BashString.make "/etc/nixos/networking.nix")
|
||||
(BashString.make "/tmp/networking.nix")
|
||||
|> fun c -> c.Stdout
|
||||
|
||||
let pullHardware =
|
||||
Command.pullFile
|
||||
config.PrivateKey
|
||||
address
|
||||
infectNix.Stdout
|
||||
"pull-hardware"
|
||||
(BashString.make "/etc/nixos/hardware-configuration.nix")
|
||||
(BashString.make "/tmp/hardware-configuration.nix")
|
||||
|> fun c -> c.Stdout
|
||||
|
||||
let! _ = pullNetworking
|
||||
Log.Info "Networking configuration at /tmp/networking.nix"
|
||||
let! _ = pullHardware
|
||||
Log.Info "Hardware configuration at /tmp/hardware.nix"
|
||||
|
||||
// TODO: do this properly via Command
|
||||
keys
|
||||
|> Array.map (fun k -> k.PublicKeyContents)
|
||||
|> Array.collect (fun s -> s.Split "\n")
|
||||
|> JsonConvert.SerializeObject
|
||||
|> fun s -> File.WriteAllText ("/tmp/ssh-keys.json", s)
|
||||
|
||||
Log.Info "Stored SSH keys at /tmp/ssh-keys.json"
|
||||
|
||||
// The nixos rebuild has blatted the known public key.
|
||||
Local.forgetKey address
|
||||
let! _ = Server.waitForReady config.PrivateKey address
|
||||
let! _ = (Local.forgetKey (address.Get ())).Stdout
|
||||
let! _ = (Local.forgetKey (string<DomainName> config.Domain)).Stderr
|
||||
let readyCommand = Server.waitForReady 2 config.PrivateKey address
|
||||
|
||||
let initialSetupModules =
|
||||
[
|
||||
yield Server.configureUser infectNix.Stdout config.RemoteUsername keys config.PrivateKey address
|
||||
yield! Server.writeFlake infectNix.Stdout config.PrivateKey address
|
||||
]
|
||||
// Reboot so that we're fully a NixOS system.
|
||||
let reboot =
|
||||
Server.reboot "initial-reboot" readyCommand.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! _ = reboot.Stdout
|
||||
|
||||
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 ()
|
||||
return address
|
||||
}
|
||||
|> ignore
|
||||
|> Deployment.RunAsync
|
||||
|
@@ -1,5 +1,5 @@
|
||||
config:
|
||||
cloudflare:apiToken:
|
||||
secure: AAABAOaQPcYG4jCFbYYr6r0dqR2f5csiAulm+GGu6EZeR1pVgqoVKUOHK3hmlW+FYUcXvnhs9Rpd9tQ15dIkplJdOp/2CEgv
|
||||
secure: AAABAFoVHnndh0tY+1jL4Z/Qml8Fy4R+RrU27w4OeWW44KoaJXSX9M4q55uqfArR2/rZZEGdJXkQoY7mluyFut5zAusuHrlX
|
||||
digitalocean:token:
|
||||
secure: AAABAAnqEO15oRMrB/9nBZaz+9ZLqo+OLz0k23QQFCS8eFgM45sGrUQIPoeCSWJ/tq+AThr8wjhe3qU6PJWxRD+zpLSHS2E/y+EH1o9WyPCi0eXeFY3uttp5ToDiVbCDiyCNVtUBwQ==
|
||||
secure: "AAABAGLyJqwR7IgpT6/7WO000SlkpsVXOX/McSOFjIGvhyfwCnGlsDlj8XUJqU+CPOzEpVtO85X/9ONno9LHGhUJtLVWcK5yhl8+/kyyGK4uii+ifImoa180nsXa/H2XCl8KjllNjw=="
|
||||
|
@@ -1,5 +1,5 @@
|
||||
config:
|
||||
cloudflare:apiToken:
|
||||
secure: AAABAK391jNLL3SDyFJBn/mBEdcZ7tUyJhwrRsdrHvckN+GzrBw5CJq4+ftaRRSIZEObTd/3wPFmoxcqgmIsiGAEBjHqLGak
|
||||
secure: AAABAHOtDVnSnpghuWApxo1FL+j1MVVjZAib4Iv9iH1bx+QQeSGDyuOFfYnxLtHsC/Ixb9CeRYgHKnsfM1Y07yYoP3i77IjK
|
||||
digitalocean:token:
|
||||
secure: AAABAIypnl37QdxXkzb8LIQvB26ncgvEjf8NgGx+KNe4rzJACTVCvvkxsf2lWG8Zf9uY2PO6WLk4qjIS6Mgm2SdQkEM1HgL2BYxyK+OGPNKb/ks9Dlw+TnkIZRVILyYlyqE7e5DRvg==
|
||||
secure: AAABAKEVauYDdDDryfXOR8Cv/eZpq4mafVcKMxTT/At3SVuN9I+aFVPlzoybee/8qzl0LwEXJ/Dh/y0IV4J6B1vEkXkH3oUjrmbt1iYESWnmTliz2m6PTwwwTBHCeD2dlXLj39mwBA==
|
||||
|
@@ -3,42 +3,51 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</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" />
|
||||
<PackageReference Include="Pulumi" Version="3.53.0" />
|
||||
<PackageReference Include="Pulumi.Cloudflare" Version="4.15.0" />
|
||||
<PackageReference Include="Pulumi.Command" Version="4.5.0" />
|
||||
<PackageReference Include="Pulumi.DigitalOcean" Version="4.16.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" />
|
||||
<None Include="Nix\nginx-config.nix" />
|
||||
<None Include="Nix\userconfig.nix" />
|
||||
<None Include="Nix\gitea-config.nix" />
|
||||
<None Include="Nix\radicale-config.nix" />
|
||||
<None Include="Nix\sops.nix" />
|
||||
<None Include="Nix\flake.nix" />
|
||||
<None Include="Nix\flake.lock" />
|
||||
<None Include="Nix\configuration.nix" />
|
||||
<None Include="Nix\hardware-configuration.nix" />
|
||||
<None Include="Nix\networking.nix" />
|
||||
<None Include="Nix\radicale\githook.sh" />
|
||||
<None Include="Nix\radicale\.gitignore" />
|
||||
<Content Include="Nix\gitea\add-user.sh" />
|
||||
<Content Include="Nix\config.json" />
|
||||
<Content Include="Nix\ssh-keys.json" />
|
||||
<Content Include="config.schema.json" />
|
||||
<Content Include="waitforready.sh">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -1,122 +0,0 @@
|
||||
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
|
||||
}
|
@@ -1,47 +1,18 @@
|
||||
namespace PulumiWebServer
|
||||
|
||||
open System
|
||||
open System.Diagnostics
|
||||
open System.IO
|
||||
open System.Reflection
|
||||
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 waitForReady (id : int) (PrivateKey privateKey) (address : Address) : Pulumi.Command.Local.Command =
|
||||
let args = Pulumi.Command.Local.CommandArgs ()
|
||||
args.Create <- Input.lift $"/bin/sh waitforready.sh {address.Get ()} {privateKey.FullName}"
|
||||
args.Dir <- Input.lift (FileInfo(Assembly.GetExecutingAssembly().Location).Directory.FullName)
|
||||
Pulumi.Command.Local.Command ($"wait-for-ready-{id}", args)
|
||||
|
||||
let infectNix (PrivateKey privateKey) (address : Address) =
|
||||
let args = CommandArgs ()
|
||||
@@ -60,102 +31,6 @@ fi && mkdir -p /preserve/nixos && cp /etc/nixos/* /preserve/nixos && touch /pres
|
||||
|
||||
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
|
||||
@@ -166,11 +41,3 @@ fi && mkdir -p /preserve/nixos && cp /etc/nixos/* /preserve/nixos && touch /pres
|
||||
"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)
|
||||
|
@@ -6,8 +6,11 @@ open System.Reflection
|
||||
[<RequireQualifiedAccess>]
|
||||
module Utils =
|
||||
let getEmbeddedResource (assembly : Assembly) (name : string) : string =
|
||||
let names = assembly.GetManifestResourceNames ()
|
||||
let names = names |> Seq.filter (fun s -> s.EndsWith name)
|
||||
|
||||
use s =
|
||||
assembly.GetManifestResourceNames ()
|
||||
names
|
||||
|> Seq.filter (fun s -> s.EndsWith name)
|
||||
|> Seq.exactlyOne
|
||||
|> assembly.GetManifestResourceStream
|
||||
|
@@ -6,9 +6,9 @@
|
||||
"required": [
|
||||
"name",
|
||||
"privateKey",
|
||||
"acmeEmail",
|
||||
"domain",
|
||||
"cnames",
|
||||
"acmeEmail",
|
||||
"remoteUsername"
|
||||
],
|
||||
"properties": {
|
||||
@@ -21,9 +21,6 @@
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"acmeEmail": {
|
||||
"type": "string"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -39,76 +36,11 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"acmeEmail": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
PulumiWebServer/waitforready.sh
Normal file
15
PulumiWebServer/waitforready.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
ADDRESS=$1
|
||||
PRIVATE_KEY=$2
|
||||
|
||||
while ! /usr/bin/ssh "root@$ADDRESS" -o ConnectTimeout=5 -o IdentityFile="$PRIVATE_KEY" -o StrictHostKeyChecking=off echo hello ; do
|
||||
echo "Sleeping for 5s"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
# 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
|
||||
sleep 10
|
||||
|
||||
date
|
14
configure.sh
14
configure.sh
@@ -11,20 +11,6 @@ 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
|
||||
|
39
deploy.sh
Executable file
39
deploy.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
# e.g. foo.bar.com (i.e. the hostname in DNS)
|
||||
# TODO: get this with `jq` from config file
|
||||
DOMAIN="$1"
|
||||
# e.g. `PulumiWebServer/Nix`, the directory holding the Nix flake that you want on the remote machine.
|
||||
# Appropriate `networking.nix`, `hardware-configuration.nix`, and `ssh-keys.json` files, as output
|
||||
# by the `pulumi up` command, will end up written to this folder.
|
||||
NIX_FLAKE="$2"
|
||||
|
||||
if [ ! -d "$NIX_FLAKE" ]; then
|
||||
echo "Flake directory $NIX_FLAKE does not exist; aborting" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO this somehow failed to find the right key
|
||||
AGE_KEY="$(ssh-keyscan "$DOMAIN" | ssh-to-age | tail -1 2>/dev/null)"
|
||||
|
||||
if [ -e "/tmp/networking.nix" ]; then
|
||||
mv "/tmp/networking.nix" "$NIX_FLAKE"
|
||||
fi
|
||||
|
||||
if [ -e "/tmp/hardware-configuration.nix" ]; then
|
||||
mv "/tmp/hardware-configuration.nix" "$NIX_FLAKE"
|
||||
fi
|
||||
|
||||
if [ -e "/tmp/ssh-keys.json" ]; then
|
||||
mv "/tmp/ssh-keys.json" "$NIX_FLAKE"
|
||||
fi
|
||||
|
||||
if [ -n "$AGE_KEY" ]; then
|
||||
sed -i -E "s! - &staging_server.+! - \&staging_server '$AGE_KEY'!g" .sops.yaml || exit 2
|
||||
fi
|
||||
|
||||
sops updatekeys "$NIX_FLAKE/secrets/staging.json" || exit 3
|
||||
|
||||
cd "$NIX_FLAKE" || exit 4
|
||||
|
||||
nixos-rebuild switch --fast --flake .#default --target-host "root@$DOMAIN" --build-host "root@$DOMAIN" || exit
|
6
flake.lock
generated
6
flake.lock
generated
@@ -17,11 +17,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1672350804,
|
||||
"narHash": "sha256-jo6zkiCabUBn3ObuKXHGqqORUMH27gYDIFFfLq5P4wg=",
|
||||
"lastModified": 1675183161,
|
||||
"narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "677ed08a50931e38382dbef01cba08a8f7eac8f6",
|
||||
"rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@@ -45,7 +45,7 @@
|
||||
flags = [];
|
||||
runtimeIds = map (system: pkgs.dotnetCorePackages.systemToDotnetRid system) dotnet-sdk.meta.platforms;
|
||||
in
|
||||
pkgs.writeShellScript "fetch-${pname}-deps" (builtins.readFile (pkgs.substituteAll {
|
||||
pkgs.writeShellScriptBin "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;})];
|
||||
@@ -78,7 +78,11 @@
|
||||
[
|
||||
pkgs.pulumi-bin
|
||||
pkgs.apacheHttpd
|
||||
pkgs.python
|
||||
pkgs.sops
|
||||
pkgs.age
|
||||
pkgs.ssh-to-age
|
||||
pkgs.nixos-rebuild
|
||||
pkgs.gnused
|
||||
]
|
||||
++ requirements;
|
||||
shellHook = ''
|
||||
|
31
nix/deps.nix
31
nix/deps.nix
@@ -21,6 +21,16 @@
|
||||
version = "5.0.0";
|
||||
sha256 = "0r535cw9ikm8xmyla6ah7qx3hb7nvz5m9fi0dqgbkd3wsrc8jlpl";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Grpc.AspNetCore.Server";
|
||||
version = "2.37.0";
|
||||
sha256 = "1wkdkfawdm5znhzwp21jwhs1hml08ks3308ak7zbf1f902jb9cad";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Grpc.Core.Api";
|
||||
version = "2.37.0";
|
||||
sha256 = "17fmhlkjn7r6jc448p3rlnqi528rpzxgdh3j9h0qmn0m1m3r19ar";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Grpc.Core.Api";
|
||||
version = "2.43.0";
|
||||
@@ -31,6 +41,11 @@
|
||||
version = "2.43.0";
|
||||
sha256 = "1yxm894lpn5sg6xg7i5ldd9bh7xg2s2c6xsx0yf7zrachy1bqbar";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Grpc.Net.Common";
|
||||
version = "2.37.0";
|
||||
sha256 = "1n15hmp5rzhpbrg8c36bd7n086dm3mgqimf6k601f78bbcym8c9r";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Grpc.Net.Common";
|
||||
version = "2.43.0";
|
||||
@@ -173,23 +188,23 @@
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Pulumi";
|
||||
version = "3.37.2";
|
||||
sha256 = "0szsw6yyvanp69yaa5cysx9m7ixn62djvk8jf68zlgrm2gngk7lr";
|
||||
version = "3.53.0";
|
||||
sha256 = "0lalidv5kbn668h4517pgzv6di6x9i9ki1xi9fabbq2p9middwpj";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Pulumi.Cloudflare";
|
||||
version = "4.9.0";
|
||||
sha256 = "1wqa059wbqzd26jn9v1rjyq70h7ypvcx549l3g1vva4b0nc699dx";
|
||||
version = "4.15.0";
|
||||
sha256 = "12wyz0slxgwwfk9ns322xgzqnix5mpx9nrmhs6rw2ssysf3jviv6";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Pulumi.Command";
|
||||
version = "0.4.1";
|
||||
sha256 = "0d6k8y2r0s6d0qjq4h6ixy6sz8lilbfpgbimk9wg8vv8drlbb2h9";
|
||||
version = "4.5.0";
|
||||
sha256 = "01m3ap6prkkq8kaqdhjcs7m5ww7jb50vybadgx26381njgf24pk0";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Pulumi.DigitalOcean";
|
||||
version = "4.14.0";
|
||||
sha256 = "17cd86nvhhcqaznz2g386fwj1vdzbqvm84x8wzqp5g1r7rdadbb3";
|
||||
version = "4.16.0";
|
||||
sha256 = "16xid3lv884csv1n4w3k0s9cqaikcbrpfavrp59c9aqc4l13a9v8";
|
||||
})
|
||||
(fetchNuGet {
|
||||
pname = "Pulumi.Protobuf";
|
||||
|
Reference in New Issue
Block a user