diff --git a/build.sbt b/build.sbt index bf0a89b..1a90ff4 100644 --- a/build.sbt +++ b/build.sbt @@ -1,11 +1,12 @@ val scala3Version = "3.4.2" -val BesomVersion = "0.3.2" -val BesomProxmoxveVersion = "6.3.1-core.0.3" val BesomCommandVersion = "0.10.0-core.0.3" +val BesomProxmoxveVersion = "6.3.1-core.0.3" +val BesomVersion = "0.3.2" val IzumiVersion = "1.2.9" -val ZioVersion = "2.1.3" +val OsLibVersion = "0.10.2" val ZioInteropCatsVersion = "23.1.0.2" +val ZioVersion = "2.1.3" inThisBuild( List( @@ -37,12 +38,13 @@ lazy val root = project name := "biser", scalaVersion := scala3Version, libraryDependencies ++= Seq( + "com.lihaoyi" %% "os-lib" % OsLibVersion, + "dev.zio" %% "zio" % ZioVersion, + "dev.zio" %% "zio-interop-cats" % ZioInteropCatsVersion, + "io.7mind.izumi" %% "distage-core" % IzumiVersion, + "org.virtuslab" %% "besom-command" % BesomCommandVersion, "org.virtuslab" %% "besom-core" % BesomVersion, - "org.virtuslab" %% "besom-zio" % BesomVersion, "org.virtuslab" %% "besom-proxmoxve" % BesomProxmoxveVersion, - "org.virtuslab" %% "besom-command" % BesomCommandVersion, - "io.7mind.izumi" %% "distage-core" % IzumiVersion, - "dev.zio" %% "zio" % ZioVersion, - "dev.zio" %% "zio-interop-cats" % ZioInteropCatsVersion + "org.virtuslab" %% "besom-zio" % BesomVersion ) ) diff --git a/src/main/scala/com/nikolaiser/biser/common/SshConfig.scala b/src/main/scala/com/nikolaiser/biser/common/SshConfig.scala new file mode 100644 index 0000000..53038d3 --- /dev/null +++ b/src/main/scala/com/nikolaiser/biser/common/SshConfig.scala @@ -0,0 +1,3 @@ +package com.nikolaiser.biser.common + +case class SshConfig(publicKey: String) diff --git a/src/main/scala/com/nikolaiser/biser/nix/FlakeBuild.scala b/src/main/scala/com/nikolaiser/biser/nix/FlakeBuild.scala deleted file mode 100644 index 9e8b6fc..0000000 --- a/src/main/scala/com/nikolaiser/biser/nix/FlakeBuild.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.nikolaiser.biser.nix - -import besom.* -import besom.api.command -import besom.internal.RegistersOutputs - -case class FlakeBuild private ( - path: Output[String] -)(using ComponentBase) - extends ComponentResource derives RegistersOutputs - -object FlakeBuild: - - case class Params( - flake: Input[String] - ) - - def apply(using Context)( - name: NonEmptyString, - params: Params, - options: ComponentResourceOptions = ComponentResourceOptions() - ): Output[FlakeBuild] = - component( - name, - "biser:nix:FlakeBuild", - options - ) { - val flakeBuildCommand = command.local.Command( - s"$name-build-command", - command.local.CommandArgs( - create = p"""nix build ${params.flake} --no-link --json 2> /dev/null | jq '.[0].outputs.out' --raw-output""" - ) - ) - - FlakeBuild(flakeBuildCommand.stdout) - } diff --git a/src/main/scala/com/nikolaiser/biser/nix/Image.scala b/src/main/scala/com/nikolaiser/biser/nix/Image.scala new file mode 100644 index 0000000..ecb71cc --- /dev/null +++ b/src/main/scala/com/nikolaiser/biser/nix/Image.scala @@ -0,0 +1,23 @@ +package com.nikolaiser.biser.nix + +import besom.* +import izumi.distage.model.definition.Id +import besom.api.command.local.Command +import besom.api.command.local.CommandArgs +import com.nikolaiser.biser.common.SshConfig + +trait Image: + + /** @return + * Path to the image file + */ + def path: Output[String] + +object Image: + case class Impl(flake: String @Id("base-image-flake"), sshConfig: SshConfig)(using Context) extends Image: + + private val cmd = + s"""purga --arg sshKey='${sshConfig.publicKey}' -- nix build $flake --no-link --refresh --json 2> /dev/null | jq '.[0].outputs.out' --raw-output""" + + val path: Output[String] = + Command(s"$flake-base-image-build", CommandArgs(create = cmd)).stdout.map(_ + "/nixos.img") diff --git a/src/main/scala/com/nikolaiser/biser/nix/PurgaDeployment.scala b/src/main/scala/com/nikolaiser/biser/nix/PurgaDeployment.scala index a747b94..8c95a41 100644 --- a/src/main/scala/com/nikolaiser/biser/nix/PurgaDeployment.scala +++ b/src/main/scala/com/nikolaiser/biser/nix/PurgaDeployment.scala @@ -1,56 +1,35 @@ package com.nikolaiser.biser.nix import besom.* -import besom.api.command -import besom.internal.RegistersOutputs +import besom.api.command.local.Command +import besom.api.command.local.CommandArgs import besom.json.JsonWriter -import scala.concurrent.Future -import Pulumi.given_ExecutionContext - -case class PurgaDeployment private ( - config: Output[String] -)(using ComponentBase) - extends ComponentResource derives RegistersOutputs - -object PurgaDeployment: - - case class Params[A]( - flake: Input[String], - flakeInput: Input[String] = "purgaArgs", - config: Input[A], - targetHost: Input[String] // including usrname@ +import besom.internal.Result +import besom.json.JsString + +def purgaDeployment[A](username: String, host: String, flake: String, config: A)(using writer: JsonWriter[A], ctx: Context) = + val jsonConfig = writer.write(config) + val deployCmd = + s"""f=$$(mktemp); echo '${jsonConfig.toString}' > $$f ; nixos-rebuild switch < /dev/null --use-remote-sudo --target-host $username@$host --show-trace --flake "$flake" --override-input purgaArgs file+file://$$f --no-write-lock-file --refresh;rm -rf $$f""" + val checkRevisionCmd = s"nix flake metadata ${flake.split("#").head} --no-write-lock-file --json --refresh | jq '.revision'" + + Command( + s"$host-$flake-deploy", + CommandArgs( + create = deployCmd, + update = deployCmd, + environment = Map( + "NIX_SSHOPTS" -> "-t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" + ), + triggers = List( + jsonConfig, + besom.internal.Output + .apply( + Result.blocking( + os.proc("/bin/sh", "-c", checkRevisionCmd).spawn().stdout.trim() + ) + ) + .map(JsString(_)) + ) + ) ) - - def apply[A: JsonWriter](using Context)( - name: NonEmptyString, - params: Params[A], - options: ComponentResourceOptions = ComponentResourceOptions() - ): Output[PurgaDeployment] = - component( - name, - "biser:nix:PurgaDeployment", - options - ) { - val jsonConfig = params.config.asOutput().map { conf => summon[JsonWriter[A]].write(conf).toString } - - val deployment = for { - - config <- jsonConfig - - _ <- command.local - .Command( - s"$name-deploy", - command.local.CommandArgs( - create = - p"""f=$$(mktemp); echo '$config' > $$f ; nixos-rebuild switch < /dev/null --use-remote-sudo --target-host ${params.targetHost} --show-trace --flake "${params.flake}";rm-rf $$f""", - environment = Map( - "NIX_SSHOPTS" -> "-t -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" - ) - ) - ) - .stdout - - } yield config - - PurgaDeployment(deployment) - } diff --git a/src/main/scala/com/nikolaiser/biser/proxmox/CloudInitVm.scala b/src/main/scala/com/nikolaiser/biser/proxmox/CloudInitVm.scala deleted file mode 100644 index bc32d6b..0000000 --- a/src/main/scala/com/nikolaiser/biser/proxmox/CloudInitVm.scala +++ /dev/null @@ -1,89 +0,0 @@ -package com.nikolaiser.biser.proxmox - -import besom.* -import besom.api.proxmoxve -import besom.internal.RegistersOutputs - -case class CloudInitVm private ( - vm: Output[proxmoxve.vm.VirtualMachine] -)(using ComponentBase) - extends ComponentResource derives RegistersOutputs - -object CloudInitVm: - - case class Params( - image: Input[proxmoxve.storage.File], - config: Input[VmConfig] - ) - - def apply(using Context)( - name: NonEmptyString, - params: Params, - options: ComponentResourceOptions = ComponentResourceOptions() - ): Output[CloudInitVm] = - component( - name, - "biser:proxmox:CloudInitVm", - options - ) { - val metaConfigFile = proxmoxve.storage.File( - s"$name-meta-config", - proxmoxve.storage.FileArgs( - contentType = "snippets", - datastoreId = "local", - nodeName = params.config.asOutput().map(_.nodeName), - sourceRaw = proxmoxve.storage.inputs.FileSourceRawArgs( - data = p""" - |dsmode: local - |local-hostname: ${params.config.asOutput().map(_.hostname)} - """.stripMargin, - fileName = p"meta-config-${params.config.asOutput().map(_.hostname)}.yaml" - ) - ) - ) - - val vm = proxmoxve.vm.VirtualMachine( - s"$name-vm", - proxmoxve.vm.VirtualMachineArgs( - name = params.config.asOutput().map(_.hostname), - nodeName = params.config.asOutput().map(_.nodeName), - tags = params.config.asOutput().map(_.tags), - agent = proxmoxve.vm.inputs.VirtualMachineAgentArgs( - enabled = params.config.asOutput().map(_.qemuAgentEnabled) - ), - cpu = proxmoxve.vm.inputs.VirtualMachineCpuArgs( - cores = params.config.asOutput().map(_.cores), - `type` = "host" - ), - memory = proxmoxve.vm.inputs.VirtualMachineMemoryArgs( - dedicated = params.config.asOutput().map(_.memoryGb * 1024) - ), - initialization = proxmoxve.vm.inputs.VirtualMachineInitializationArgs( - userAccount = proxmoxve.vm.inputs.VirtualMachineInitializationUserAccountArgs( - username = params.config.asOutput().map(_.username), - keys = params.config.asOutput().map(_.authorizedKeys) - ), - ipConfigs = List( - proxmoxve.vm.inputs.VirtualMachineInitializationIpConfigArgs( - ipv4 = proxmoxve.vm.inputs - .VirtualMachineInitializationIpConfigIpv4Args( - address = params.config.asOutput().map(_.ipv4Config.map(_.address).getOrElse("dhcp")), - gateway = params.config.asOutput().map(_.ipv4Config.map(_.gateway)) - ) - ) - ), - metaDataFileId = metaConfigFile.id - ), - disks = List( - proxmoxve.vm.inputs.VirtualMachineDiskArgs( - datastoreId = "local-lvm", - fileId = params.image.asOutput().id, - interface = "virtio0", - size = params.config.asOutput().map(_.diskGb) - ) - ) - ) - ) - - CloudInitVm(vm) - } diff --git a/src/main/scala/com/nikolaiser/biser/proxmox/NixOsCloudInit.scala b/src/main/scala/com/nikolaiser/biser/proxmox/NixOsCloudInit.scala deleted file mode 100644 index 5f12146..0000000 --- a/src/main/scala/com/nikolaiser/biser/proxmox/NixOsCloudInit.scala +++ /dev/null @@ -1,48 +0,0 @@ -package com.nikolaiser.biser.proxmox - -import besom.* -import besom.api.proxmoxve -import besom.internal.RegistersOutputs -import com.nikolaiser.biser.nix.FlakeBuild - -case class NixOsCloudInit private ( - file: Output[proxmoxve.storage.File] -)(using ComponentBase) - extends ComponentResource derives RegistersOutputs - -object NixOsCloudInit: - - case class Params( - flake: Input[String], - nodeName: Input[String] - ) - - def apply(using Context)( - name: NonEmptyString, - params: Params, - options: ComponentResourceOptions = ComponentResourceOptions() - ): Output[NixOsCloudInit] = - component( - name, - "biser:proxmox:NixOsCloudInit", - options - ) { - val imageBuild = FlakeBuild( - s"$name-image-build", - FlakeBuild.Params(params.flake) - ) - - val cloudInitImageFile = proxmoxve.storage.File( - s"$name-image-upload", - proxmoxve.storage.FileArgs( - contentType = "snippets", - datastoreId = "local", - nodeName = params.nodeName, - sourceFile = proxmoxve.storage.inputs.FileSourceFileArgs( - path = p"${imageBuild.flatMap(_.path)}/nixos.img" - ) - ) - ) - - NixOsCloudInit(cloudInitImageFile) - } diff --git a/src/main/scala/com/nikolaiser/biser/proxmox/NodeImage.scala b/src/main/scala/com/nikolaiser/biser/proxmox/NodeImage.scala new file mode 100644 index 0000000..70628c0 --- /dev/null +++ b/src/main/scala/com/nikolaiser/biser/proxmox/NodeImage.scala @@ -0,0 +1,35 @@ +package com.nikolaiser.biser.proxmox + +import besom.* +import besom.api.proxmoxve.storage.File +import besom.api.proxmoxve.storage.FileArgs +import besom.api.proxmoxve.storage.inputs.FileSourceFileArgs +import besom.api.proxmoxve.Provider +import com.nikolaiser.biser.nix.Image +import izumi.distage.model.definition.With + +trait NodeImage: + def file: Output[File] + +object NodeImage: + + type Factory = String => NodeImage @With[NodeImage.Impl] + + case class Impl( + nodeName: String, + baseImage: Image, + provider: Provider + )(using + Context + ) extends NodeImage: + val file: Output[File] = + File( + s"$nodeName-image-upload", + FileArgs( + contentType = "snippets", + datastoreId = "local", + nodeName = nodeName, + sourceFile = FileSourceFileArgs(path = baseImage.path) + ), + opts(provider = provider) + ) diff --git a/src/main/scala/com/nikolaiser/biser/proxmox/ProxmoxVm.scala b/src/main/scala/com/nikolaiser/biser/proxmox/ProxmoxVm.scala new file mode 100644 index 0000000..c3d9c2e --- /dev/null +++ b/src/main/scala/com/nikolaiser/biser/proxmox/ProxmoxVm.scala @@ -0,0 +1,50 @@ +package com.nikolaiser.biser.proxmox + +import besom.* +import besom.api.proxmoxve.Provider +import besom.api.proxmoxve.vm.VirtualMachine +import com.nikolaiser.biser.common.SshConfig +import besom.api.proxmoxve.vm.VirtualMachineArgs +import besom.api.proxmoxve.vm.inputs.VirtualMachineAgentArgs +import besom.api.proxmoxve.vm.inputs.VirtualMachineCpuArgs +import besom.api.proxmoxve.vm.inputs.VirtualMachineMemoryArgs +import besom.api.proxmoxve.vm.inputs.VirtualMachineDiskArgs +import izumi.distage.model.definition.With + +trait ProxmoxVm: + def vm: Output[VirtualMachine] + +object ProxmoxVm: + + type Factory = VmInstanceParams => ProxmoxVm @With[Impl] + + case class Impl( + vmInstanceParams: VmInstanceParams, + sshConfig: SshConfig, + vmConfig: VmConfig, + nodeImageFactory: NodeImage.Factory, + provider: Provider + )(using Context) + extends ProxmoxVm: + def nodeBaseImageId = nodeImageFactory(vmInstanceParams.nodeName).file.id + + def vm: Output[VirtualMachine] = VirtualMachine( + s"${vmInstanceParams.nodeName}-${vmInstanceParams.name}-vm", + VirtualMachineArgs( + name = s"${vmInstanceParams.nodeName}-${vmInstanceParams.name}", + nodeName = vmInstanceParams.nodeName, + tags = vmConfig.tags, + agent = VirtualMachineAgentArgs(enabled = vmConfig.qemuAgentEnabled), + cpu = VirtualMachineCpuArgs(cores = vmConfig.cores, `type` = "host"), + memory = VirtualMachineMemoryArgs(dedicated = vmConfig.memoryGb * 1024), + disks = List( + VirtualMachineDiskArgs( + datastoreId = "local-lvm", + fileId = nodeBaseImageId, + interface = "virtio0", + size = vmConfig.diskGb + ) + ) + ), + opts(provider = provider, ignoreChanges = List("disks[0].speed", "cdrom")) + ) diff --git a/src/main/scala/com/nikolaiser/biser/proxmox/VmConfig.scala b/src/main/scala/com/nikolaiser/biser/proxmox/VmConfig.scala index bfc735f..34ee1f4 100644 --- a/src/main/scala/com/nikolaiser/biser/proxmox/VmConfig.scala +++ b/src/main/scala/com/nikolaiser/biser/proxmox/VmConfig.scala @@ -1,16 +1,10 @@ package com.nikolaiser.biser.proxmox case class VmConfig( - hostname: String, - nodeName: String, cores: Int, memoryGb: Int, diskGb: Int, username: String = "ops", - authorizedKeys: List[String] = Nil, - ipv4Config: Option[IpV4Config] = None, tags: List[String] = Nil, qemuAgentEnabled: Boolean = true ) - -case class IpV4Config(address: String, gateway: String) diff --git a/src/main/scala/com/nikolaiser/biser/proxmox/VmInstanceParams.scala b/src/main/scala/com/nikolaiser/biser/proxmox/VmInstanceParams.scala new file mode 100644 index 0000000..1fe0d15 --- /dev/null +++ b/src/main/scala/com/nikolaiser/biser/proxmox/VmInstanceParams.scala @@ -0,0 +1,6 @@ +package com.nikolaiser.biser.proxmox + +case class VmInstanceParams( + name: String, + nodeName: String +)