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

Add GCP Cloudrun scala-steward example #545

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions examples/gcp-cloudrun-scala-steward/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
### Scala an JVM
*.class
*.log
.bsp
.scala-build

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
31 changes: 31 additions & 0 deletions examples/gcp-cloudrun-scala-steward/GCPService.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import besom.*
import besom.api.gcp
import gcp.projects.{Service, ServiceArgs}

sealed trait GCPService(private val name: String):
def apply(
disableDependentServices: Input[Boolean] = true,
disableOnDestroy: Input[Boolean] = true
)(using Context): Output[Service] =
GCPService.projectService(s"$name.googleapis.com", disableDependentServices, disableOnDestroy)

object GCPService:
case object CloudRun extends GCPService("run")
case object Scheduler extends GCPService("cloudscheduler")
case object SecretManager extends GCPService("secretmanager")

private def projectService(
name: String,
disableDependentServices: Input[Boolean] = true,
disableOnDestroy: Input[Boolean] = true
)(using Context): Output[Service] =
Service(
s"enable-${name.replace(".", "-")}",
ServiceArgs(
service = name,
// if true - at every destroy this will disable the dependent services for the whole project
disableDependentServices = disableDependentServices,
// if true - at every destroy this will disable the service for the whole project
disableOnDestroy = disableOnDestroy
)
)
16 changes: 16 additions & 0 deletions examples/gcp-cloudrun-scala-steward/GcpConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import besom.*

case class GcpConfig(
project: Output[String],
region: Output[String]
)
object GcpConfig:
extension (o: Output[GcpConfig])
def project: Output[String] = o.flatMap(_.project)
def region: Output[String] = o.flatMap(_.region)

def apply(using Context): GcpConfig =
GcpConfig(
project = config.requireString("gcp:project"),
region = config.requireString("gcp:region")
)
13 changes: 13 additions & 0 deletions examples/gcp-cloudrun-scala-steward/GitAskPassFile.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
object GitAskPassFile:
private val passFileName = "pass.sh"
private val gitPassFolderPath = "/opt/git"
val gitPassEnvName = "GIT_PASSWORD"
val gitPassPath = s"$gitPassFolderPath/$passFileName"

val gitPassFile: String =
List(
s"mkdir $gitPassFolderPath",
s"echo '#!/bin/sh' >> $gitPassPath",
s"echo 'echo $$$gitPassEnvName' >> $gitPassPath",
s"chmod +x $gitPassPath"
).mkString(" && ")
17 changes: 17 additions & 0 deletions examples/gcp-cloudrun-scala-steward/GitConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import besom.*
import besom.json.*

case class GitConfig(
forgeLogin: String,
forgeApiHost: String,
forgeType: String,
gitAuthorEmail: String,
password: String
) derives JsonFormat
object GitConfig:
extension (o: Output[GitConfig])
def forgeLogin: Output[String] = o.map(_.forgeLogin)
def forgeApiHost: Output[String] = o.map(_.forgeApiHost)
def forgeType: Output[String] = o.map(_.forgeType)
def gitAuthorEmail: Output[String] = o.map(_.gitAuthorEmail)
def password: Output[String] = o.map(_.password)
185 changes: 185 additions & 0 deletions examples/gcp-cloudrun-scala-steward/Main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import besom.*
import besom.api.gcp
import besom.api.gcp.cloudrunv2.inputs.*
import besom.api.gcp.cloudrunv2.{Job, JobArgs, JobIamMember, JobIamMemberArgs}
import besom.api.gcp.cloudscheduler as csh
import besom.api.gcp.cloudscheduler.inputs.*
import besom.api.gcp.secretmanager.*
import besom.api.gcp.secretmanager.inputs.*
import besom.api.gcp.serviceaccount.{Account, AccountArgs}
import besom.api.gcp.storage.{Bucket, BucketArgs, BucketObject, BucketObjectArgs, BucketIamMember, BucketIamMemberArgs}

@main def main = Pulumi.run {
val repoFileName = "repos.md"
val volumePath = "/opt/scala-steward"
val appName = "scala-steward"
val scalaStewardVersion = "latest"
val gitConfig = config.requireObject[GitConfig]("git")
val gcpConfig = GcpConfig.apply

val serviceAccount = Account(
s"$appName-sa",
AccountArgs(
accountId = s"$appName-sa",
displayName = "Service Account for Scala Steward"
)
)
val serviceAccountMember = p"serviceAccount:${serviceAccount.email}"

// Create a Cloud Storage bucket
val bucket = Bucket(
s"$appName-bucket",
BucketArgs(
location = "US",
forceDestroy = true
)
)

// Grant the Cloud Run job service account permissions to access and delete objects in the bucket
val bucketIamMember = BucketIamMember(
s"$appName-bucket-access",
BucketIamMemberArgs(
bucket = bucket.name,
role = "roles/storage.objectAdmin", // This role includes permissions to delete objects
member = serviceAccountMember
)
)

val reposObject = BucketObject(
s"$appName-repos",
BucketObjectArgs(
bucket = bucket.name,
name = repoFileName,
source = besom.Asset.FileAsset(s"./$repoFileName")
)
)

val secret = Secret(
s"$appName-secret",
SecretArgs(
secretId = s"$appName-git-secret",
replication = SecretReplicationArgs(
auto = SecretReplicationAutoArgs()
)
),
opts(dependsOn = GCPService.SecretManager())
)

val secretVersion = SecretVersion(
s"$appName-secret-version",
SecretVersionArgs(
secret = secret.name,
secretData = gitConfig.password
)
)

val secretIamMember = SecretIamMember(
s"$appName-secret-access",
SecretIamMemberArgs(
secretId = secret.id,
role = "roles/secretmanager.secretAccessor",
member = serviceAccountMember
),
opts(dependsOn = secret)
)

val scalaStewardRun =
List(
p"/opt/docker/bin/scala-steward",
p"--workspace $volumePath/workspace",
p"--repos-file $volumePath/${reposObject.name}",
p"--git-author-email ${gitConfig.gitAuthorEmail}",
p"--forge-type ${gitConfig.forgeType}",
p"--forge-api-host ${gitConfig.forgeApiHost}",
p"--forge-login ${gitConfig.forgeLogin}",
p"--git-ask-pass ${GitAskPassFile.gitPassPath}",
p"--do-not-fork"
).sequence.map(_.mkString(" "))

// Define the Cloud Run Job
val cloudRunJob = Job(
s"$appName-job",
JobArgs(
location = gcpConfig.region,
template = JobTemplateArgs(
template = JobTemplateTemplateArgs(
serviceAccount = serviceAccount.email,
timeout = "2400s", // 40 min
maxRetries = 2,
containers = JobTemplateTemplateContainerArgs(
// Scala Steward Docker image
image = p"fthomas/scala-steward:$scalaStewardVersion",
envs = JobTemplateTemplateContainerEnvArgs(
name = GitAskPassFile.gitPassEnvName,
valueSource = JobTemplateTemplateContainerEnvValueSourceArgs(
secretKeyRef = JobTemplateTemplateContainerEnvValueSourceSecretKeyRefArgs(
secret = secret.id,
version = secretVersion.version
)
)
) :: Nil,
commands = List(
"/bin/sh",
"-c",
p"${GitAskPassFile.gitPassFile} && $scalaStewardRun"
),
resources = JobTemplateTemplateContainerResourcesArgs(
limits = Map("cpu" -> "2", "memory" -> "8Gi")
),
volumeMounts = JobTemplateTemplateContainerVolumeMountArgs(
name = "gcs-volume",
mountPath = volumePath
) :: Nil
) :: Nil,
volumes = JobTemplateTemplateVolumeArgs(
name = "gcs-volume",
gcs = JobTemplateTemplateVolumeGcsArgs(
bucket = bucket.name,
readOnly = false
)
) :: Nil
)
)
),
opts(dependsOn = GCPService.CloudRun())
)

// Grant the Cloud Scheduler service account permission to invoke the Cloud Run job
val jobIamMember = JobIamMember(
s"$appName-scheduler-invoker",
JobIamMemberArgs(
name = cloudRunJob.name,
role = "roles/run.invoker",
member = serviceAccountMember
)
)

val schedulerUri =
p"https://${gcpConfig.region}-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/${gcpConfig.project}/jobs/${cloudRunJob.name}:run"
// Create a Cloud Scheduler job to trigger the Cloud Run job
val schedulerJob = csh.Job(
s"$appName-scheduler",
csh.JobArgs(
schedule = "0 12 * * *",
timeZone = "Etc/UTC",
httpTarget = JobHttpTargetArgs(
httpMethod = "POST",
uri = schedulerUri,
oauthToken = JobHttpTargetOauthTokenArgs(
serviceAccountEmail = serviceAccount.email
)
)
),
opts(dependsOn = GCPService.Scheduler())
)

Stack(bucketIamMember, secretIamMember, jobIamMember)
.exports(
jobName = cloudRunJob.name,
gitPasswordFile = GitAskPassFile.gitPassFile,
scalaStewardRunScript = scalaStewardRun,
schedulerJobName = schedulerJob.name,
secretName = secret.name,
secretVersionName = secretVersion.name
)
}
3 changes: 3 additions & 0 deletions examples/gcp-cloudrun-scala-steward/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: gcp-cloudrun-scala-steward
runtime: scala
description: Scala Steward on Google Cloud Run
114 changes: 114 additions & 0 deletions examples/gcp-cloudrun-scala-steward/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Scala Steward on Google Cloud Run

A Scala application that uses Google Cloud Run Job to
run [Scala Steward](https://github.com/scala-steward-org/scala-steward/tree/main) job with scheduler.

## Prerequisites

1. **Install Pulumi CLI**:

To install the latest Pulumi release, run the following (see
[installation instructions](https://www.pulumi.com/docs/reference/install/) for additional installation options):

```bash
curl -fsSL https://get.pulumi.com/ | sh
```

2. **Install Scala CLI**:

To install the latest Scala CLI release, run the following (see
[installation instructions](https://scala-cli.virtuslab.org/install) for additional installation options):

```bash
curl -sSLf https://scala-cli.virtuslab.org/get | sh
```

3. **Install Scala Language Plugin in Pulumi**:

To install the latest Scala Language Plugin release, run the following:

```bash
pulumi plugin install language scala 0.3.2 --server github://api.github.com/VirtusLab/besom
```
If you not do this, you see this error:\
`error: failed to load language plugin scala: no language plugin 'pulumi-language-scala' found in the workspace or on your $PATH`


4. [**Install Google CLI**](https://cloud.google.com/sdk/docs/install)


5. **Authenticate and configure GCP**:

```bash
gcloud auth application-default login
```

6. **Create access token** and add it to `git:password` (see below) with scope: api, read_repository, write_repository.\
[Example of creating access token on Gitlab](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html)

## Deploying and running the program

1. Create a new stack:

```bash
pulumi stack init dev
```

2. Set the required configuration variables:

Set project name:
```bash
pulumi config set gcp:project <value>
```
Set region. Any valid GCP region here but preferred `us-east1, us-west1, or us-central1` because of [**Free Tier
**](https://cloud.google.com/free/docs/free-cloud-features#free-tier):
```bash
pulumi config set gcp:region us-east1
```
Set password to git repository. Recommended an authentication token:
```bash
pulumi config set git:password <secret-value> --secret
```
Add below lines to `Pulumi.dev.yaml` file. Fill with proper values:
```yaml
scala-steward:git:
forgeApiHost: <value>
forgeLogin: <value>
forgeType: <value>
gitAuthorEmail: <value>
password:
secure: <secret-value> # secret value from above command
```
You may delete config `git:password <secret-value>` from `Pulumi.dev.yaml` created earlier.


3. Run `pulumi up` to preview and deploy changes. After the preview is shown you will be prompted if you want to
continue or not.

```bash
pulumi up
```
**Warning**: The first launch of the app may take longer, so you may need to change the cloud run job timeout.
Default is set to 40 min (`2400s`)


4. To see the output that was created, run:

```bash
pulumi stack output
```

5. On GCP console check logs and see if scala-steward is working properly.

6. To clean up resources, remove docker images, destroy your stack and remove it:

```bash
pulumi down
```
```bash
pulumi stack rm dev
```
If you want to revoke your Application Default Credentials:
```bash
gcloud auth application-default revoke
```
Loading
Loading