Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

besom-cfg initial checkin to monorepo #494

Merged
merged 8 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,6 @@ jobs:

- name: Integration test
run: scala-cli --power test integration-tests

- name: Test besom-cfg
run: just test-cfg
62 changes: 61 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Big idea behind using a Justfile is so that we can have modules like in sbt.

besom-version := `cat version.txt`
besom-cfg-version := `cat besom-cfg/version.txt`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume there is a reason behind versioning cfg separately, but wouldn't it make sense to align the versions, just for the sake of simplicity?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it wouldn't, this stuff has and has to have a completely separate lifecycle

is-snapshot := if "{{besom-version}}" =~ '.*-SNAPSHOT' { "true" } else { "false" }
no-bloop-ci := if env_var_or_default('CI', "") == "true" { "--server=false" } else { "" }

Expand Down Expand Up @@ -35,8 +36,10 @@ default:
# Aggregate tasks
####################

# TODO aggregate tasks do not incorporate besom-cfg module (with the exception of clean-all)

# Cleans everything
clean-all: clean-json clean-sdk clean-auto clean-out clean-compiler-plugin clean-codegen clean-scripts clean-test-integration clean-test-templates clean-test-examples clean-test-markdown
clean-all: clean-json clean-sdk clean-auto clean-out clean-compiler-plugin clean-codegen clean-scripts clean-test-integration clean-cfg clean-test-templates clean-test-examples clean-test-markdown

# Compiles everything
compile-all: compile-json compile-sdk compile-auto compile-codegen compile-scripts compile-compiler-plugin build-language-plugin
Expand Down Expand Up @@ -280,6 +283,63 @@ publish-language-plugins-all: package-language-plugins-all
just publish-language-plugin windows arm64
just publish-language-plugin windows amd64

####################
# Besom CFG
####################

# Compiles besom-cfg lib module
compile-cfg-lib: publish-local-json publish-local-core
scala-cli --power compile besom-cfg/lib --suppress-experimental-feature-warning

# Compiles besom-cfg k8s module
compile-cfg-k8s: publish-local-cfg-lib
just cli packages local kubernetes:4.10.0
scala-cli --power compile besom-cfg/k8s --suppress-experimental-feature-warning

# Compiles all besom-cfg modules
compile-cfg: compile-cfg-lib compile-cfg-k8s

# Publishes locally besom-cfg lib module
publish-local-cfg-lib:
scala-cli --power publish local besom-cfg/lib --project-version {{besom-cfg-version}} --suppress-experimental-feature-warning

# Publishes locally besom-cfg k8s module
publish-local-cfg-k8s: compile-cfg-k8s
scala-cli --power publish local besom-cfg/k8s --project-version {{besom-cfg-version}} --suppress-experimental-feature-warning

# Publishes locally all besom-cfg modules
publish-local-cfg: publish-local-cfg-lib publish-local-cfg-k8s

# Publishes besom-cfg lib module to Maven
publish-maven-cfg-lib:
scala-cli --power publish besom-cfg/lib --project-version {{besom-cfg-version}} {{publish-maven-auth-options}} --suppress-experimental-feature-warning

# Publishes besom-cfg k8s module to Maven
publish-maven-cfg-k8s:
scala-cli --power publish besom-cfg/k8s --project-version {{besom-cfg-version}} {{publish-maven-auth-options}} --suppress-experimental-feature-warning

# Tests besom-cfg lib module
test-cfg-lib: compile-cfg-lib
scala-cli --power test besom-cfg/lib --suppress-experimental-feature-warning

# Tests besom-cfg k8s module
test-cfg-k8s: publish-local-cfg-lib compile-cfg-k8s
scala-cli --power test besom-cfg/k8s --suppress-experimental-feature-warning

# Runs all tests of besom-cfg
test-cfg: test-cfg-lib test-cfg-k8s

# Cleans besom-cfg-lib build
clean-cfg-lib:
scala-cli clean besom-cfg/lib

# Cleans besom-cfg-k8s build
clean-cfg-k8s:
scala-cli clean besom-cfg/k8s

# Cleans all besom-cfg builds
clean-cfg: clean-cfg-lib clean-cfg-k8s

####################
# Codegen
####################
Expand Down
11 changes: 11 additions & 0 deletions besom-cfg/k8s/.scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version = 3.5.2
runner.dialect = scala3
project.git = true
align = most
align.openParenCallSite = false
align.openParenDefnSite = false
align.tokens = [{code = "=>", owner = "Case"}, "<-", "%", "%%", "="]
indent.defnSite = 2
maxColumn = 140

rewrite.scala3.insertEndMarkerMinLines = 40
23 changes: 23 additions & 0 deletions besom-cfg/k8s/project.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//> using scala 3.3.3

//> using dep com.lihaoyi::os-lib::0.9.3
//> using dep org.virtuslab::besom-cfg:0.2.0-SNAPSHOT
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be replaced by current besom version next time you run the version bump script, that's why I was suggesting the version alignment ;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so there has to be a way to provide exceptions in the version bump script

//> using dep org.virtuslab::besom-kubernetes::4.10.0-core.0.4-SNAPSHOT
//> using dep com.lihaoyi::fansi::0.5.0
//> using dep com.lihaoyi::fastparse:3.1.0

//> using test.resourceDir ./src/test/resources

//> using test.dep com.lihaoyi::pprint:0.6.6
//> using test.dep org.scalameta::munit:1.0.0-M11

//> using publish.name "besom-cfg-k8s"
//> using publish.organization "org.virtuslab"
//> using publish.url "https://github.com/VirtusLab/besom"
//> using publish.vcs "github:VirtusLab/besom"
//> using publish.license "Apache-2.0"
//> using publish.repository "central"
//> using publish.developer "lbialy|Łukasz Biały|https://github.com/lbialy"
//> using publish.developer "prolativ|Michał Pałka|https://github.com/prolativ"
//> using publish.developer "KacperFKorban|Kacper Korban|https://github.com/KacperFKorban"
//> using publish.developer "pawelprazak|Paweł Prażak|https://github.com/pawelprazak"
172 changes: 172 additions & 0 deletions besom-cfg/k8s/src/main/scala/ConfiguredContainerArgs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package besom.cfg.k8s

import besom.cfg.internal.*
import besom.types.{Input, Context, Output}
import besom.cfg.*
import besom.json.*
import besom.cfg.containers.*
import besom.api.kubernetes.core.v1.inputs.*

import scala.util.*
import scala.quoted.*
import besom.cfg.k8s.syntax.*

// this is besom-cfg-kubernetes entrypoint
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is the entrypoint, I'd strongly advice adding even the most barebones scaladoc :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll strongly consider that once besom-cfg is beyond a prototype


object syntax:
extension (s: Struct)
def foldedToEnvVarArgs(using Context): Output[List[EnvVarArgs]] =
s.foldToEnv.map(_.map { case (k, v) => EnvVarArgs(name = k, value = v) })

object ConfiguredContainerArgs:

private val NL = System.lineSeparator()

inline def apply[C <: Struct](
name: String,
image: String,
configuration: C,
args: Input.Optional[List[Input[String]]] = None,
command: Input.Optional[List[Input[String]]] = None,
env: Input.Optional[List[Input[EnvVarArgs]]] = None,
envFrom: Input.Optional[List[Input[EnvFromSourceArgs]]] = None,
imagePullPolicy: Input.Optional[String] = None,
lifecycle: Input.Optional[LifecycleArgs] = None,
livenessProbe: Input.Optional[ProbeArgs] = None,
ports: Input.Optional[List[Input[ContainerPortArgs]]] = None,
readinessProbe: Input.Optional[ProbeArgs] = None,
resizePolicy: Input.Optional[List[Input[ContainerResizePolicyArgs]]] = None,
resources: Input.Optional[ResourceRequirementsArgs] = None,
restartPolicy: Input.Optional[String] = None,
securityContext: Input.Optional[SecurityContextArgs] = None,
startupProbe: Input.Optional[ProbeArgs] = None,
stdin: Input.Optional[Boolean] = None,
stdinOnce: Input.Optional[Boolean] = None,
terminationMessagePath: Input.Optional[String] = None,
terminationMessagePolicy: Input.Optional[String] = None,
tty: Input.Optional[Boolean] = None,
volumeDevices: Input.Optional[List[Input[VolumeDeviceArgs]]] = None,
volumeMounts: Input.Optional[List[Input[VolumeMountArgs]]] = None,
workingDir: Input.Optional[String] = None
)(using ctx: Context) = ${
applyImpl(
'name,
'image,
'configuration,
'args,
'command,
'env,
'envFrom,
'imagePullPolicy,
'lifecycle,
'livenessProbe,
'ports,
'readinessProbe,
'resizePolicy,
'resources,
'restartPolicy,
'securityContext,
'startupProbe,
'stdin,
'stdinOnce,
'terminationMessagePath,
'terminationMessagePolicy,
'tty,
'volumeDevices,
'volumeMounts,
'workingDir,
'ctx
)
}

def applyImpl[C <: Struct: Type](
name: Expr[String],
image: Expr[String],
configuration: Expr[C],
args: Expr[Input.Optional[List[Input[String]]]],
command: Expr[Input.Optional[List[Input[String]]]],
env: Expr[Input.Optional[List[Input[EnvVarArgs]]]],
envFrom: Expr[Input.Optional[List[Input[EnvFromSourceArgs]]]],
imagePullPolicy: Expr[Input.Optional[String]],
lifecycle: Expr[Input.Optional[LifecycleArgs]],
livenessProbe: Expr[Input.Optional[ProbeArgs]],
ports: Expr[Input.Optional[List[Input[ContainerPortArgs]]]],
readinessProbe: Expr[Input.Optional[ProbeArgs]],
resizePolicy: Expr[Input.Optional[List[Input[ContainerResizePolicyArgs]]]],
resources: Expr[Input.Optional[ResourceRequirementsArgs]],
restartPolicy: Expr[Input.Optional[String]],
securityContext: Expr[Input.Optional[SecurityContextArgs]],
startupProbe: Expr[Input.Optional[ProbeArgs]],
stdin: Expr[Input.Optional[Boolean]],
stdinOnce: Expr[Input.Optional[Boolean]],
terminationMessagePath: Expr[Input.Optional[String]],
terminationMessagePolicy: Expr[Input.Optional[String]],
tty: Expr[Input.Optional[Boolean]],
volumeDevices: Expr[Input.Optional[List[Input[VolumeDeviceArgs]]]],
volumeMounts: Expr[Input.Optional[List[Input[VolumeMountArgs]]]],
workingDir: Expr[Input.Optional[String]],
context: Expr[Context]
)(using Quotes): Expr[ContainerArgs] =
import quotes.reflect.*

val contName = name.value match
case None => report.errorAndAbort("Container name has to be a literal!", name)
case Some(value) => value

val dockerImage = image.value match
case None => report.errorAndAbort("Image name has to be a literal!", image)
case Some(value) => value

val schema = getDockerImageMetadata(dockerImage) match
case Left(throwable) => report.errorAndAbort(s"Failed to get metadata for image $dockerImage:$NL${pprint(throwable)}", image)
case Right(schema) => schema

Diff.performDiff(schema, configuration) match
case Left(prettyDiff) => // TODO maybe strip all the ansi codes if in CI?
report.errorAndAbort(
s"Configuration provided for container $contName ($dockerImage) is invalid:$NL$NL$prettyDiff",
configuration
)

case Right(()) =>
val envExpr = '{
val envOutput = ${ env }.asOptionOutput()(using ${ context })
val conf = ${ configuration }
val configurationAsEnvVarArgs = conf.foldedToEnvVarArgs(using $context)
envOutput.zip(configurationAsEnvVarArgs).map {
case (Some(envVarArgsList), envVarArgsListFromConf) => envVarArgsList ++ envVarArgsListFromConf
case (None, envVarArgsListFromConf) => envVarArgsListFromConf
}
}

'{
ContainerArgs(
args = $args,
command = $command,
env = $envExpr,
envFrom = $envFrom,
image = $image,
imagePullPolicy = $imagePullPolicy,
lifecycle = $lifecycle,
livenessProbe = $livenessProbe,
name = ${ Expr(contName) },
ports = $ports,
readinessProbe = $readinessProbe,
resizePolicy = $resizePolicy,
resources = $resources,
restartPolicy = $restartPolicy,
securityContext = $securityContext,
startupProbe = $startupProbe,
stdin = $stdin,
stdinOnce = $stdinOnce,
terminationMessagePath = $terminationMessagePath,
terminationMessagePolicy = $terminationMessagePolicy,
tty = $tty,
volumeDevices = $volumeDevices,
volumeMounts = $volumeMounts,
workingDir = $workingDir
)(using $context)
}
end match
end applyImpl
end ConfiguredContainerArgs
63 changes: 63 additions & 0 deletions besom-cfg/k8s/src/main/scala/containers.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package besom.cfg.containers

// this should be a separate package, base for all container integrations

import besom.cfg.internal.Schema
import besom.json.*
import scala.util.Try

val cacheDir = sys.props.get("java.io.tmpdir").getOrElse("/tmp")

def sanitizeImageName(image: String): String =
image
.replace("/", "_")
.replace(":", "_")

def fetchFromCache(image: String): Option[String] =
if image.endsWith(":latest") then None
else
val sanitized = sanitizeImageName(image)
os.makeDir.all(os.Path(s"$cacheDir/besom-cfg"))
Try(os.read(os.Path(s"$cacheDir/besom-cfg/$sanitized"))).toOption

def saveToCache(image: String, content: String): Unit =
if !image.endsWith(":latest") then
val sanitized = sanitizeImageName(image)
os.makeDir.all(os.Path(s"$cacheDir/besom-cfg"))
os.write.over(os.Path(s"$cacheDir/besom-cfg/$sanitized"), content)

def resolveMetadataFromImage(image: String): String =
lazy val sbtNativePackagerFormatCall =
os
.proc("docker", "run", "--rm", "--entrypoint", "java", image, "-cp", "/opt/docker/lib/*", "besom.cfg.SummonConfiguration")
.call(check = false)

lazy val customDockerFormatCall =
os
.proc("docker", "run", "--rm", "--entrypoint", "java", image, "-cp", "/app/main", "besom.cfg.SummonConfiguration")
.call(check = false)

if sbtNativePackagerFormatCall.exitCode == 0 then sbtNativePackagerFormatCall.out.text().trim()
else if customDockerFormatCall.exitCode == 0 then customDockerFormatCall.out.text().trim()
else throw RuntimeException(s"Failed to get configuration from $image")

def getDockerImageMetadata(image: String): Either[Throwable, Schema] =
Try {
// 1. cache result per image in /tmp DONE
// 2. verify the version of the library used, fail macro if we are older than it
// 3. parse the json to correct structure DONE
// next:
// - support different image setups, autodetect which one is used somehow? somewhat DONE
// - cp argument should be configurable
val json = fetchFromCache(image) match {
case Some(cachedJson) => cachedJson
case None =>
val json = resolveMetadataFromImage(image)

saveToCache(image, json)

json
}

summon[JsonFormat[Schema]].read(json.parseJson)
}.toEither
Loading
Loading