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

Towards Auto API #336

Merged
merged 15 commits into from
May 22, 2024
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
just-version: '1.14.0' # just for sanity
- uses: pulumi/actions@v4
with:
pulumi-version: '3.94.2'
pulumi-version: '3.116.1'
- uses: coursier/cache-action@v6.4.3
- uses: VirtusLab/scala-cli-setup@v1.1.0
with:
Expand Down
37 changes: 31 additions & 6 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,19 @@ default:
####################

# Cleans everything
clean-all: clean-json clean-sdk 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-test-templates clean-test-examples clean-test-markdown

# Compiles everything
compile-all: compile-json compile-sdk compile-codegen compile-scripts compile-compiler-plugin build-language-plugin
compile-all: compile-json compile-sdk compile-auto compile-codegen compile-scripts compile-compiler-plugin build-language-plugin

# Tests everything
test-all: test-json test-sdk test-codegen test-scripts test-integration test-templates test-examples test-markdown
test-all: test-json test-sdk test-auto test-codegen test-scripts test-integration test-templates test-examples test-markdown

# Publishes everything locally
publish-local-all: publish-local-json publish-local-sdk publish-local-codegen publish-local-scripts install-language-plugin
publish-local-all: publish-local-json publish-local-sdk publish-local-auto publish-local-codegen publish-local-scripts install-language-plugin

# Publishes everything to Maven
publish-maven-all: publish-maven-json publish-maven-sdk publish-maven-codegen publish-maven-scripts
publish-maven-all: publish-maven-json publish-maven-sdk publish-maven-auto publish-maven-codegen publish-maven-scripts

# Runs all necessary checks before committing
before-commit: compile-all test-all
Expand Down Expand Up @@ -168,7 +168,7 @@ compile-json:

# Runs tests for json module
test-json:
scala-cli --power test besom-json --suppress-experimental-feature-warning -v -v -v
scala-cli --power test besom-json --suppress-experimental-feature-warning

# Cleans json module
clean-json:
Expand All @@ -182,6 +182,31 @@ publish-local-json:
publish-maven-json:
scala-cli --power publish besom-json --project-version {{besom-version}} {{publish-maven-auth-options}} --suppress-experimental-feature-warning


####################
# Auto
####################

# Compiles auto module
compile-auto: publish-local-core
scala-cli --power compile auto --suppress-experimental-feature-warning

# Runs tests for auto module
test-auto: compile-auto
scala-cli --power test auto --suppress-experimental-feature-warning

# Cleans auto module
clean-auto:
scala-cli --power clean auto

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

# Publishes auto module
publish-maven-auto: test-auto
scala-cli --power publish auto --project-version {{besom-version}} {{publish-maven-auth-options}}

####################
# Language plugin
####################
Expand Down
11 changes: 11 additions & 0 deletions auto/.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
25 changes: 25 additions & 0 deletions auto/project.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//> using scala 3.3.1
//> using options -java-output-version:11
//> using options -deprecation -feature -Werror -Wunused:all

//> using dep org.virtuslab::besom-json:0.4.0-SNAPSHOT
//> using dep org.virtuslab::besom-core:0.4.0-SNAPSHOT
//> using dep org.virtuslab::scala-yaml:0.0.8
//> using dep com.lihaoyi::os-lib:0.10.0
//> using dep com.lihaoyi::os-lib-watch:0.10.0
//> using dep org.eclipse.jgit:org.eclipse.jgit:6.8.0.202311291450-r
//> using dep org.eclipse.jgit:org.eclipse.jgit.ssh.jsch:6.8.0.202311291450-r
//> using dep org.slf4j:slf4j-nop:2.0.13
//> using dep ma.chinespirit::tailf:0.1.0

//> using test.dep org.scalameta::munit:1.0.0-M10

//> using publish.name "besom-auto"
//> 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 "pawelprazak|Paweł Prażak|https://github.com/pawelprazak"
//> using repository sonatype:snapshots
142 changes: 142 additions & 0 deletions auto/src/main/scala/besom/auto/internal/AutoError.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package besom.auto.internal

import scala.util.matching.Regex

@SerialVersionUID(1L)
sealed abstract class BaseAutoError(message: Option[String], cause: Option[Throwable])
extends Exception(message.orElse(cause.map(_.toString)).orNull, cause.orNull)
with Product
with Serializable

@SerialVersionUID(1L)
case class AutoError(message: Option[String], cause: Option[Throwable]) extends BaseAutoError(message, cause)
object AutoError:
def apply(message: String) = new AutoError(Some(message), None)
def apply(message: String, cause: Throwable) = new AutoError(Some(message), Some(cause))
def apply(cause: Throwable) = new AutoError(None, Some(cause))

@SerialVersionUID(1L)
case class ShellAutoError(
message: Option[String],
cause: Option[Throwable],
exitCode: Int,
stdout: String,
stderr: String,
command: Seq[String],
envVars: Map[String, String]
) extends BaseAutoError(message, cause):
def withMessage(message: String): ShellAutoError = copy(message = this.message.map(message + "; " + _).orElse(Some(message)))

/** Returns true if the error was a result of selecting a stack that does not exist.
*
* @return
* `true` if the error was due to a non-existent stack.
*/
def isSelectStack404Error: Boolean =
val regex: Regex = "no stack named.*found".r
regex.findFirstIn(stderr).isDefined

/** Returns true if the error was a result of a conflicting update locking the stack.
*
* @return
* `true` if the error was due to a conflicting update.
*/
def isConcurrentUpdateError: Boolean =
val conflictText = "[409] Conflict: Another update is currently in progress."
val localBackendConflictText = "the stack is currently locked by"
stderr.contains(conflictText) || stderr.contains(localBackendConflictText)

/** Returns true if the error was a result of creating a stack that already exists.
*
* @return
* `true` if the error was due to a stack that already exists.
*/
def isCreateStack409Error: Boolean =
val regex: Regex = "stack.*already exists".r
regex.findFirstIn(stderr).isDefined

/** Returns true if the pulumi core engine encountered an error (most likely a bug).
*
* @return
* `true` if the error was due to an unexpected engine error.
*/
def isUnexpectedEngineError: Boolean =
stdout.contains("The Pulumi CLI encountered a fatal error. This is a bug!")

end ShellAutoError
object ShellAutoError:
def apply(message: String, exitCode: Int, stdout: String, stderr: String, command: Seq[String], envVars: Map[String, String]) =
new ShellAutoError(
Some(msg(Some(message), None, exitCode, stdout, stderr, command, envVars)),
None,
exitCode,
stdout,
stderr,
command,
envVars
)
def apply(
message: String,
cause: Throwable,
exitCode: Int,
stdout: String,
stderr: String,
command: Seq[String],
envVars: Map[String, String]
) =
new ShellAutoError(
Some(msg(Some(message), Some(cause), exitCode, stdout, stderr, command, envVars)),
Some(cause),
exitCode,
stdout,
stderr,
command,
envVars
)
def apply(cause: Throwable, exitCode: Int, stdout: String, stderr: String, command: Seq[String], envVars: Map[String, String]) =
new ShellAutoError(
Some(msg(None, Some(cause), exitCode, stdout, stderr, command, envVars)),
Some(cause),
exitCode,
stdout,
stderr,
command,
envVars
)
def apply(exitCode: Int, stdout: String, stderr: String, command: Seq[String], envVars: Map[String, String]) =
new ShellAutoError(
Some(msg(None, None, exitCode, stdout, stderr, command, envVars)),
None,
exitCode,
stdout,
stderr,
command,
envVars
)

private def msg(
message: Option[String],
cause: Option[Throwable],
exitCode: Int,
stdout: String,
stderr: String,
command: Seq[String],
envVars: Map[String, String]
): String = {
s"""|${message.map(_ + "\n").getOrElse("")}${cause.map("cause: " + _.getMessage + "\n").getOrElse("")}
|command: ${redacted(command)}
|code: $exitCode
|stdout:
|$stdout
|stderr:
|$stderr
|env:
|${envVars.map { case (k, v) => s" $k=$v" }.mkString("\n")}
|""".stripMargin
}

private def redacted(command: Seq[String]) =
val parts = if command.contains("-secret") then command.take(2) :+ "...[REDACTED]" else command
parts.mkString(" ")

end ShellAutoError
141 changes: 141 additions & 0 deletions auto/src/main/scala/besom/auto/internal/Git.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package besom.auto.internal

import besom.util.*
import com.jcraft.jsch.JSch
import org.eclipse.jgit.api.{CloneCommand, TransportConfigCallback}
import org.eclipse.jgit.lib.ObjectId
import org.eclipse.jgit.revwalk.RevWalk
import org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory
import org.eclipse.jgit.transport.{CredentialsProvider, RefSpec, SshTransport, UsernamePasswordCredentialsProvider}

import java.util.Collections
import scala.util.Using

object Git:
private val RefPrefix = "refs/"
private val RefHeadPrefix = RefPrefix + "heads/"
private val RefTagPrefix = RefPrefix + "tags/"
private val RefRemotePrefix = RefPrefix + "remotes/"

private class RichCloneCommand(private var depth: Option[Int] = None) extends CloneCommand:
def getCredentialsProvider: CredentialsProvider = credentialsProvider
def getTransportConfigCallback: TransportConfigCallback = transportConfigCallback
def getDepth: Option[Int] = depth
override def setDepth(depth: Int): CloneCommand =
val cmd = super.setDepth(depth)
this.depth = Some(depth)
cmd

def setupGitRepo(workDir: os.Path, repoArgs: GitRepo): Either[Exception, os.Path] =
try
val cloneCommand = new RichCloneCommand()

cloneCommand
.setRemote("origin") // be explicit so we can require it in remote refs
.setURI(repoArgs.url)
.setDirectory(workDir.toIO)

if repoArgs.shallow then
cloneCommand
.setDepth(1)
.setCloneAllBranches(false)

repoArgs.auth match
case GitAuth.UsernameAndPassword(username, password) =>
cloneCommand.setCredentialsProvider(new UsernamePasswordCredentialsProvider(username, password))
case GitAuth.PersonalAccessToken(token) =>
// With Personal Access Token the username for use with a PAT can be
// *anything* but an empty string so we are setting this to 'git'
cloneCommand.setCredentialsProvider(new UsernamePasswordCredentialsProvider("git", token))
case GitAuth.SSHPrivateKey(key, passphrase) =>
val sshSessionFactory = new JschConfigSessionFactory():
override protected def configureJSch(jsch: JSch): Unit =
jsch.removeAllIdentity()
jsch.addIdentity("key", key.getBytes(), null /* no pub key */, passphrase.asOption.map(_.getBytes()).orNull)
cloneCommand.setTransportConfigCallback { transport =>
transport.asInstanceOf[SshTransport].setSshSessionFactory(sshSessionFactory)
}
case GitAuth.SSHPrivateKeyPath(keyPath, passphrase) =>
val sshSessionFactory = new JschConfigSessionFactory():
override protected def configureJSch(jsch: JSch): Unit =
jsch.removeAllIdentity()
jsch.addIdentity(keyPath, passphrase.asOption.map(_.getBytes()).orNull)
cloneCommand.setTransportConfigCallback { transport =>
transport.asInstanceOf[SshTransport].setSshSessionFactory(sshSessionFactory)
}
case NotProvided => // do nothing
repoArgs.branch match
case branch: String =>
// `Git.cloneRepository` will do appropriate fetching given a branch name. We must deal with
// different varieties, since people have been advised to use these as a workaround while only
// "refs/heads/<default>" worked.
//
// If a reference name is not supplied, then clone will fetch all refs (and all objects
// referenced by those), and checking out a commit later will work as expected.
val ref = {
try
val refSpec = RefSpec(branch)
if refSpec.matchSource(RefRemotePrefix)
then
refSpec.getDestination match
case s"origin/$branch" => s"$RefHeadPrefix/$branch"
case _ => throw AutoError("a remote ref must begin with 'refs/remote/origin/', but got: '$branch'")
else if refSpec.matchSource(RefTagPrefix) then
branch // looks like `refs/tags/v1.0.0` -- respect this even though the field is `.Branch`
else if !refSpec.matchSource(RefHeadPrefix) then
s"$RefHeadPrefix/$branch" // not a remote, not refs/heads/branch; treat as a simple branch name
else
// already looks like a full branch name or tag, so use as is
refSpec.toString
catch case e: IllegalArgumentException => throw AutoError(s"Invalid branch name: '$branch'", e)
}
cloneCommand.setBranchesToClone(Collections.singletonList(ref))

case NotProvided => // do nothing
end match
// NOTE: pulumi has a workaround here for Azure DevOps requires, we might add if needed
val git = cloneCommand.call()
val repository = git.getRepository

repoArgs.commitHash match
case commitHash: String =>
// ensure that the commit has been fetched
val fetchCommand = git
.fetch()
.setRemote("origin")
.setRefSpecs(new RefSpec(s"$commitHash:$commitHash"))
.setCredentialsProvider(cloneCommand.getCredentialsProvider)
.setTransportConfigCallback(cloneCommand.getTransportConfigCallback)

cloneCommand.getDepth.foreach {
fetchCommand.setDepth(_)
}
val _ = fetchCommand.call()

// If a commit hash is provided, then we must check it out explicitly. Otherwise, the
// repository will be in a detached HEAD state, and the commit hash will be the only
// commit in the repository.
val commitId = ObjectId.fromString(commitHash)
Using.resource(new RevWalk(repository)) { revWalk =>
val commit = revWalk.parseCommit(commitId)
val _ = git
.checkout()
.setName(commit.getName)
.setForced(true) // this method guarantees 'git --force' semantics
.call()
}
case NotProvided => // do nothing
end match

val finalWorkDir =
if repoArgs.projectPath.asOption.nonEmpty then
val projectPath = os.rel / repoArgs.projectPath.asOption.get
workDir / projectPath
else workDir

Right(finalWorkDir)

catch case e: Exception => Left(e)

end setupGitRepo
end Git
Loading
Loading