Skip to content

Commit 42a9d8f

Browse files
committed
feat: export of draft changes back to git
1 parent e042e55 commit 42a9d8f

File tree

9 files changed

+245
-11
lines changed

9 files changed

+245
-11
lines changed

gradle.properties

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ org.gradle.caching=true
22

33
# Supported MPS versions by Modelix workspaces.
44
# Docker images are built for each of the versions.
5-
mpsMajorVersions=2020.3,2021.1,2021.2,2021.3,2022.2,2022.3,2023.2,2023.3,2024.1
5+
mpsMajorVersions=2021.1,2021.2,2021.3,2022.2,2022.3,2023.2,2023.3,2024.1
66

77
# For each supported major MPS version define:
88
# * The exact MPS version including the minor version.
9-
mpsVersion2020.3=2020.3.6
109
mpsVersion2021.1=2021.1.4
1110
mpsVersion2021.2=2021.2.6
1211
mpsVersion2021.3=2021.3.5

gradle/libs.versions.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ kotlinSerialization="1.9.0"
1010
kotlinx-coroutines = "1.10.2"
1111
ktor = "3.3.1"
1212
logback = "1.5.20"
13-
modelix-core = "16.2.3"
13+
modelix-core = "16.5.0"
1414
modelix-mps-plugins = "0.12.0"
15-
modelix-openapi = "1.3.0"
15+
modelix-openapi = "1.4.0"
1616

1717
[libraries]
1818
auth0-jwt = { group = "com.auth0", name = "java-jwt", version = "4.5.0" }
@@ -68,7 +68,7 @@ maven-invoker = { group = "org.apache.maven.shared", name = "maven-invoker", ver
6868
modelix-api-client-ktor = { group = "org.modelix", name = "api-client-ktor", version.ref = "modelix-openapi" }
6969
modelix-api-server-stubs = { group = "org.modelix", name = "api-server-stubs-ktor", version.ref = "modelix-openapi" }
7070
modelix-authorization = { group = "org.modelix", name = "authorization", version.ref = "modelix-core" }
71-
modelix-dashboard = { group = "org.modelix", name = "dashboard-spa", version = "1.1.1" }
71+
modelix-dashboard = { group = "org.modelix", name = "dashboard-spa", version = "1.2.0" }
7272
modelix-model-client = { group = "org.modelix", name = "model-client", version.ref = "modelix-core" }
7373
modelix-model-server = { group = "org.modelix", name = "model-server", version.ref = "modelix-core" }
7474
modelix-mps-build-tools = { group = "org.modelix.mps", name="build-tools-lib", version = "2.0.1"}

helm/modelix/values.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ oauthProxy:
1717
modelServer:
1818
image:
1919
repository: modelix/model-server
20-
tag: "16.2.3"
20+
tag: "16.5.0"
2121
pullPolicy: IfNotPresent
2222

2323
gitImport:
2424
image:
2525
repository: modelix/mps-git-import
26-
tag: "16.2.3"
26+
tag: "16.5.0"
2727
pullPolicy: IfNotPresent
2828

2929
proxy:

workspace-manager/src/main/kotlin/org/modelix/services/gitconnector/GitConnectorController.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import io.ktor.server.routing.Route
88
import kotlinx.serialization.Serializable
99
import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorDraftsController
1010
import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorDraftsController.Companion.modelixGitConnectorDraftsRoutes
11+
import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorDraftsExportJobController
12+
import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorDraftsExportJobController.Companion.modelixGitConnectorDraftsExportJobRoutes
1113
import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorDraftsPreparationJobController
1214
import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorDraftsPreparationJobController.Companion.modelixGitConnectorDraftsPreparationJobRoutes
1315
import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorDraftsRebaseJobController
@@ -25,6 +27,7 @@ import org.modelix.services.gitconnector.stubs.controllers.ModelixGitConnectorRe
2527
import org.modelix.services.gitconnector.stubs.controllers.TypedApplicationCall
2628
import org.modelix.services.gitconnector.stubs.models.DraftConfig
2729
import org.modelix.services.gitconnector.stubs.models.DraftConfigList
30+
import org.modelix.services.gitconnector.stubs.models.DraftExportJob
2831
import org.modelix.services.gitconnector.stubs.models.DraftPreparationJob
2932
import org.modelix.services.gitconnector.stubs.models.DraftRebaseJob
3033
import org.modelix.services.gitconnector.stubs.models.GitBranchList
@@ -304,6 +307,43 @@ class GitConnectorController(val manager: GitConnectorManager) {
304307
call.respondJob(task)
305308
}
306309
})
310+
311+
modelixGitConnectorDraftsExportJobRoutes(object : ModelixGitConnectorDraftsExportJobController {
312+
suspend fun TypedApplicationCall<DraftExportJob>.respondJob(task: GitExportTask) {
313+
respondTyped(
314+
DraftExportJob(
315+
active = when (task.getState()) {
316+
TaskState.CREATED, TaskState.ACTIVE -> true
317+
TaskState.CANCELLED, TaskState.COMPLETED, TaskState.UNKNOWN -> false
318+
},
319+
errorMessage = task.getOutput()?.exceptionOrNull()?.stackTraceToString(),
320+
gitBranchName = task.gitBranchName,
321+
modelixVersionHash = task.key.modelixVersionHash,
322+
),
323+
)
324+
}
325+
326+
override suspend fun getDraftExportJob(
327+
draftId: String,
328+
call: TypedApplicationCall<DraftExportJob>,
329+
) {
330+
val draft = manager.getDraft(draftId)
331+
?: return call.respondText("Draft not found: $draftId", status = HttpStatusCode.NotFound)
332+
val task = manager.exportTasks.getAll().lastOrNull { it.key.modelixBranchName == draft.modelixBranchName }
333+
?: return call.respondText("No export job found for draft $draftId", status = HttpStatusCode.NotFound)
334+
call.respondJob(task)
335+
}
336+
337+
override suspend fun exportDraft(
338+
draftId: String,
339+
draftExportJob: DraftExportJob,
340+
call: TypedApplicationCall<DraftExportJob>,
341+
) {
342+
val task = manager.getOrCreateExportTask(draftId)
343+
task.launch()
344+
call.respondJob(task)
345+
}
346+
})
307347
}
308348
}
309349

workspace-manager/src/main/kotlin/org/modelix/services/gitconnector/GitConnectorManager.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class GitConnectorManager(
2727
) {
2828

2929
private val importTasks = ReusableTasks<GitImportTask.Key, GitImportTask>()
30+
val exportTasks = ReusableTasks<GitExportTask.Key, GitExportTask>()
3031
val draftPreparationTasks = ReusableTasks<DraftPreparationTask.Key, DraftPreparationTask>()
3132
private val draftRebaseTasks = ReusableTasks<DraftRebaseTask.Key, DraftRebaseTask>()
3233

@@ -63,6 +64,32 @@ class GitConnectorManager(
6364
}
6465
}
6566

67+
suspend fun getOrCreateExportTask(draftId: String): GitExportTask {
68+
val data = connectorData.getValue()
69+
val draft = requireNotNull(data.drafts[draftId]) { "Draft not found: $draftId" }
70+
val gitRepoConfig = requireNotNull(data.repositories[draft.gitRepositoryId]) { "Repository not found: ${draft.gitRepositoryId}" }
71+
val modelixBranch = gitRepoConfig.getModelixRepositoryId().getBranchReference(draft.modelixBranchName)
72+
val versionHash = modelClient.pullHash(modelixBranch)
73+
val key = GitExportTask.Key(
74+
repo = gitRepoConfig,
75+
modelixVersionHash = versionHash,
76+
modelixBranchName = draft.modelixBranchName,
77+
gitBaseBranch = draft.gitBranchName,
78+
)
79+
return getOrCreateExportTask(key)
80+
}
81+
82+
fun getOrCreateExportTask(taskKey: GitExportTask.Key): GitExportTask {
83+
return exportTasks.getOrCreateTask(taskKey) {
84+
GitExportTask(
85+
key = taskKey,
86+
scope = scope,
87+
modelClient = modelClient,
88+
jwtUtil = kestraClient.jwtUtil,
89+
)
90+
}
91+
}
92+
6693
fun getOrCreateDraftPreparationTask(draftId: String): DraftPreparationTask {
6794
val key = DraftPreparationTask.Key(
6895
draftId = draftId,
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package org.modelix.services.gitconnector
2+
3+
import io.kubernetes.client.openapi.models.V1Container
4+
import io.kubernetes.client.openapi.models.V1EnvVar
5+
import io.kubernetes.client.openapi.models.V1Job
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.withContext
9+
import org.eclipse.jgit.api.Git
10+
import org.modelix.authorization.ModelixJWTUtil
11+
import org.modelix.model.client2.IModelClientV2
12+
import org.modelix.model.lazy.RepositoryId
13+
import org.modelix.model.server.ModelServerPermissionSchema
14+
import org.modelix.services.gitconnector.stubs.models.GitRepositoryConfig
15+
import org.modelix.services.workspaces.metadata
16+
import org.modelix.services.workspaces.spec
17+
import org.modelix.services.workspaces.template
18+
import org.modelix.workspace.manager.ITaskInstance
19+
import org.modelix.workspace.manager.KubernetesJobTask
20+
import java.time.ZoneId
21+
import java.time.format.DateTimeFormatter
22+
import kotlin.time.Clock
23+
import kotlin.time.Duration
24+
import kotlin.time.Duration.Companion.seconds
25+
import kotlin.time.ExperimentalTime
26+
import kotlin.time.toJavaInstant
27+
28+
class GitExportTask(
29+
val key: Key,
30+
scope: CoroutineScope,
31+
val modelClient: IModelClientV2,
32+
val jwtUtil: ModelixJWTUtil,
33+
) : ITaskInstance<FetchedBranch>, KubernetesJobTask<FetchedBranch>(scope) {
34+
35+
companion object {
36+
@OptIn(ExperimentalTime::class)
37+
fun timeForBranchName() = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
38+
.withZone(ZoneId.systemDefault())
39+
.format(Clock.System.now().toJavaInstant())
40+
}
41+
42+
data class Key(
43+
val repo: GitRepositoryConfig,
44+
val modelixVersionHash: String,
45+
val modelixBranchName: String,
46+
val gitBaseBranch: String,
47+
)
48+
49+
private val repoId = requireNotNull(key.repo.modelixRepository?.let { RepositoryId(it) }) { "Repository ID missing" }
50+
private val modelixBranch = repoId.getBranchReference(key.modelixBranchName)
51+
val gitBranchName = "modelix-export/${key.gitBaseBranch}/${timeForBranchName()}-${key.modelixVersionHash.replace("*", "")}"
52+
53+
private fun chooseRemote() = requireNotNull(key.repo.remotes?.firstOrNull()) { "No remotes specified" }
54+
55+
override fun getResultCheckingInterval(): Duration = 30.seconds
56+
57+
override suspend fun tryGetResult(): FetchedBranch? {
58+
val remote = chooseRemote()
59+
val cmd = Git.lsRemoteRepository()
60+
cmd.setRemote(remote.url)
61+
cmd.setHeads(true)
62+
cmd.setTags(false)
63+
64+
val username = remote.credentials?.username.orEmpty()
65+
val password = remote.credentials?.password.orEmpty()
66+
if (password.isNotEmpty()) {
67+
cmd.applyCredentials(username, password)
68+
}
69+
cmd.configureHttpProxy()
70+
71+
val refs = withContext(Dispatchers.IO) {
72+
cmd.call()
73+
}
74+
75+
for (ref in refs) {
76+
if (!ref.name.startsWith("refs/heads/")) continue
77+
val branchName = ref.name.removePrefix("refs/heads/")
78+
if (branchName == this.gitBranchName) {
79+
return FetchedBranch(
80+
remoteName = remote.name,
81+
branchName = branchName,
82+
commitHash = ref.objectId.name,
83+
)
84+
}
85+
}
86+
return null
87+
}
88+
89+
@Suppress("ktlint")
90+
override fun generateJobYaml(): V1Job {
91+
val remote = chooseRemote()
92+
val token = jwtUtil.createAccessToken(
93+
94+
listOf(
95+
ModelServerPermissionSchema.repository(modelixBranch.repositoryId).read.fullId,
96+
ModelServerPermissionSchema.branch(modelixBranch).read.fullId,
97+
),
98+
)
99+
100+
return V1Job().apply {
101+
metadata {
102+
name = "gitexportjob-$id"
103+
}
104+
spec {
105+
template {
106+
spec {
107+
addContainersItem(V1Container().apply {
108+
name = "importer"
109+
image = System.getenv("GIT_IMPORT_IMAGE")
110+
System.getenv("MODELIX_HTTP_PROXY")?.let {
111+
addEnvItem(V1EnvVar().name("MODELIX_HTTP_PROXY").value(it))
112+
}
113+
args = listOf(
114+
"git-export-remote",
115+
remote.url,
116+
"--git-user",
117+
remote.credentials?.username,
118+
"--git-password",
119+
remote.credentials?.password,
120+
"--model-server",
121+
System.getenv("model_server_url"),
122+
"--token",
123+
token,
124+
"--modelix-repository",
125+
modelixBranch.repositoryId.id,
126+
"--modelix-branch",
127+
modelixBranch.branchName,
128+
"--version",
129+
key.modelixVersionHash,
130+
"--git-branch",
131+
gitBranchName,
132+
)
133+
})
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}

workspace-manager/src/main/kotlin/org/modelix/services/gitconnector/GitImportTask.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ class GitImportTaskUsingKubernetesJob(
4242

4343
override suspend fun tryGetResult(): IVersion? {
4444
return if (modelixBranchExists()) {
45-
return modelClient.lazyLoadVersion(branchRef).takeIf { it.gitCommit == key.gitRevision }
45+
modelClient.lazyLoadVersion(branchRef).takeIf { it.gitCommit == key.gitRevision }
4646
} else {
47-
return null
47+
null
4848
}
4949
}
5050

workspace-manager/src/main/kotlin/org/modelix/workspace/manager/KubernetesJobTask.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,35 @@ import org.modelix.services.workspaces.spec
1414
import org.modelix.services.workspaces.template
1515
import org.modelix.workspace.manager.WorkspaceJobQueue.Companion.KUBERNETES_NAMESPACE
1616
import kotlin.coroutines.suspendCoroutine
17+
import kotlin.time.Clock
18+
import kotlin.time.Duration
1719
import kotlin.time.Duration.Companion.minutes
20+
import kotlin.time.Duration.Companion.seconds
21+
import kotlin.time.ExperimentalTime
22+
import kotlin.time.Instant
1823

24+
@OptIn(ExperimentalTime::class)
1925
abstract class KubernetesJobTask<Out : Any>(scope: CoroutineScope) : TaskInstance<Out>(scope) {
2026
companion object {
2127
const val JOB_ID_LABEL = "modelix.workspace.job.id"
2228
private val LOG = mu.KotlinLogging.logger {}
2329
}
2430

31+
private var lastResultCheck: Instant = Instant.fromEpochSeconds(0L)
32+
2533
abstract suspend fun tryGetResult(): Out?
2634
abstract fun generateJobYaml(): V1Job
35+
open fun getResultCheckingInterval(): Duration = 5.seconds
36+
37+
suspend fun checkForResult(): Out? {
38+
val now = Clock.System.now()
39+
if (now - lastResultCheck < getResultCheckingInterval()) return null
40+
lastResultCheck = Clock.System.now()
41+
return tryGetResult()
42+
}
2743

2844
override suspend fun process() = withTimeout(30.minutes) {
29-
tryGetResult()?.let { return@withTimeout it }
45+
checkForResult()?.let { return@withTimeout it }
3046

3147
findJob()?.let { deleteJob(it) }
3248
createJob()
@@ -35,7 +51,7 @@ abstract class KubernetesJobTask<Out : Any>(scope: CoroutineScope) : TaskInstanc
3551
while (true) {
3652
delay(1000)
3753

38-
tryGetResult()?.let { return@withTimeout it }
54+
checkForResult()?.let { return@withTimeout it }
3955

4056
val job = findJob()
4157

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.modelix.workspace.manager
2+
3+
import org.modelix.services.gitconnector.GitExportTask
4+
import kotlin.test.Test
5+
import kotlin.test.assertTrue
6+
7+
class GitExportTaskTest {
8+
9+
@Test
10+
fun `timestamp format`() {
11+
assertTrue(GitExportTask.timeForBranchName().matches(Regex("""\d{8}-\d{6}""")))
12+
}
13+
}

0 commit comments

Comments
 (0)