diff --git a/deploy/crd/giteaactionrunners.glasskube.eu-v1.yml b/deploy/crd/giteaactionrunners.glasskube.eu-v1.yml new file mode 100644 index 00000000..0e5a2f0e --- /dev/null +++ b/deploy/crd/giteaactionrunners.glasskube.eu-v1.yml @@ -0,0 +1,40 @@ +# Generated by Fabric8 CRDGenerator, manual edits might get overwritten! +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: giteaactionrunners.glasskube.eu +spec: + group: glasskube.eu + names: + kind: GiteaActionRunner + plural: giteaactionrunners + singular: giteaactionrunner + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + properties: + spec: + properties: + token: + type: string + gitea: + properties: + name: + type: string + type: object + required: + - token + - gitea + type: object + status: + properties: + ready: + type: boolean + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/crd/giteas.glasskube.eu-v1.yml b/deploy/crd/giteas.glasskube.eu-v1.yml index 174eba97..37f92f23 100644 --- a/deploy/crd/giteas.glasskube.eu-v1.yml +++ b/deploy/crd/giteas.glasskube.eu-v1.yml @@ -193,6 +193,20 @@ spec: required: - s3 type: object + actions: + properties: + enabled: + type: boolean + runners: + items: + properties: + token: + type: string + required: + - token + type: object + type: array + type: object required: - host type: object diff --git a/operator/src/main/kotlin/eu/glasskube/kubernetes/api/model/apps/RollingUpdateStatefulSetStrategyDsl.kt b/operator/src/main/kotlin/eu/glasskube/kubernetes/api/model/apps/RollingUpdateStatefulSetStrategyDsl.kt index a748bb16..1f39d78f 100644 --- a/operator/src/main/kotlin/eu/glasskube/kubernetes/api/model/apps/RollingUpdateStatefulSetStrategyDsl.kt +++ b/operator/src/main/kotlin/eu/glasskube/kubernetes/api/model/apps/RollingUpdateStatefulSetStrategyDsl.kt @@ -1,9 +1,11 @@ package eu.glasskube.kubernetes.api.model.apps +import eu.glasskube.kubernetes.api.annotation.KubernetesDslMarker import eu.glasskube.kubernetes.api.model.intOrString import io.fabric8.kubernetes.api.model.apps.RollingUpdateStatefulSetStrategy import io.fabric8.kubernetes.api.model.apps.RollingUpdateStatefulSetStrategyBuilder +@KubernetesDslMarker class RollingUpdateStatefulSetStrategyDsl private constructor() { private val builder = RollingUpdateStatefulSetStrategyBuilder(true) diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaActionRunnerSpecTemplate.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaActionRunnerSpecTemplate.kt new file mode 100644 index 00000000..c99f7705 --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaActionRunnerSpecTemplate.kt @@ -0,0 +1,11 @@ +package eu.glasskube.operator.apps.gitea + +import io.fabric8.generator.annotation.Required + +data class GiteaActionRunnerSpecTemplate( + @field:Required + val token: String +) + +val GiteaActionRunnerSpecTemplate.tokenHash: String + get() = (token.hashCode().toLong() + Int.MAX_VALUE).toString(16) diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaActionsSpec.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaActionsSpec.kt new file mode 100644 index 00000000..b49fdc73 --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaActionsSpec.kt @@ -0,0 +1,6 @@ +package eu.glasskube.operator.apps.gitea + +data class GiteaActionsSpec( + val enabled: Boolean = false, + val runners: List = emptyList() +) diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaReconciler.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaReconciler.kt index be992e2a..ac5d84c8 100644 --- a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaReconciler.kt +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaReconciler.kt @@ -4,6 +4,7 @@ import eu.glasskube.kubernetes.client.patchOrUpdateStatus import eu.glasskube.operator.Labels import eu.glasskube.operator.api.reconciler.informerEventSource import eu.glasskube.operator.api.reconciler.secondaryResource +import eu.glasskube.operator.apps.gitea.dependent.GiteaActionRunners import eu.glasskube.operator.apps.gitea.dependent.GiteaConfigMap import eu.glasskube.operator.apps.gitea.dependent.GiteaDeployment import eu.glasskube.operator.apps.gitea.dependent.GiteaHttpService @@ -85,7 +86,8 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers type = GiteaDeployment::class, name = "GiteaDeployment", dependsOn = ["GiteaPostgresCluster", "GiteaVolume", "GiteaSecret", "GiteaConfigMap", "GiteaIniConfigMap", "GiteaRedisService"], - useEventSourceWithName = GiteaReconciler.DEPLOYMENT_EVENT_SOURCE + useEventSourceWithName = GiteaReconciler.DEPLOYMENT_EVENT_SOURCE, + readyPostcondition = GiteaDeployment.ReadyCondition::class ), Dependent( type = GiteaHttpService::class, @@ -107,6 +109,11 @@ import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers name = "GiteaServiceMonitor", dependsOn = ["GiteaHttpService"] ), + Dependent( + type = GiteaActionRunners::class, + name = "GiteaActionRunners", + dependsOn = ["GiteaDeployment"] + ), Dependent( type = GiteaVeleroSecret::class, name = "GiteaVeleroSecret", diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaSpec.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaSpec.kt index bb30d85e..1001df1f 100644 --- a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaSpec.kt +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/GiteaSpec.kt @@ -33,5 +33,6 @@ data class GiteaSpec( val version: String = "1.20.4", @field:Nullable override val database: PostgresDatabaseSpec = PostgresDatabaseSpec(), - override val backups: BackupSpec? + override val backups: BackupSpec?, + val actions: GiteaActionsSpec = GiteaActionsSpec() ) : HasBackupSpec, HasDatabaseSpec diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaActionRunners.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaActionRunners.kt new file mode 100644 index 00000000..d9bb237c --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaActionRunners.kt @@ -0,0 +1,57 @@ +package eu.glasskube.operator.apps.gitea.dependent + +import eu.glasskube.kubernetes.api.model.metadata +import eu.glasskube.kubernetes.api.model.namespace +import eu.glasskube.operator.apps.gitea.Gitea +import eu.glasskube.operator.apps.gitea.GiteaActionRunnerSpecTemplate +import eu.glasskube.operator.apps.gitea.GiteaReconciler +import eu.glasskube.operator.apps.gitea.resourceLabels +import eu.glasskube.operator.apps.gitea.runner.GiteaActionRunner +import eu.glasskube.operator.apps.gitea.runner.GiteaActionRunnerSpec +import eu.glasskube.operator.apps.gitea.runner.giteaActionRunner +import eu.glasskube.operator.apps.gitea.tokenHash +import io.fabric8.kubernetes.api.model.LocalObjectReference +import io.javaoperatorsdk.operator.api.reconciler.Context +import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource +import io.javaoperatorsdk.operator.processing.dependent.Matcher +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent + +@KubernetesDependent(labelSelector = GiteaReconciler.SELECTOR) +class GiteaActionRunners : + CRUDKubernetesDependentResource(GiteaActionRunner::class.java), + BulkDependentResource { + + fun desired(primary: Gitea, template: GiteaActionRunnerSpecTemplate) = giteaActionRunner { + metadata { + name("${primary.metadata.name}-${template.tokenHash}") + namespace(primary.namespace) + labels(primary.resourceLabels) + } + spec = GiteaActionRunnerSpec( + template.token, + LocalObjectReference(primary.metadata.name) + ) + } + + override fun desiredResources(primary: Gitea, context: Context) = primary.spec.actions.run { + if (enabled) { + runners.map { desired(primary, it) }.associateBy { it.metadata.name } + } else { + emptyMap() + } + } + + override fun getSecondaryResources(primary: Gitea, context: Context) = + context.getSecondaryResources(GiteaActionRunner::class.java) + .filter { it.spec.gitea.name == primary.metadata.name } + .associateBy { it.metadata.name } + + override fun match( + actualResource: GiteaActionRunner, + desired: GiteaActionRunner, + primary: Gitea, + context: Context + ): Matcher.Result = + super.match(actualResource, desired, primary, context) +} diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaDeployment.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaDeployment.kt index d6e44e2c..e781e35a 100644 --- a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaDeployment.kt +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaDeployment.kt @@ -33,6 +33,7 @@ import eu.glasskube.operator.apps.gitea.resourceLabelSelector import eu.glasskube.operator.apps.gitea.resourceLabels import eu.glasskube.operator.apps.gitea.secretName import eu.glasskube.operator.config.ConfigService +import eu.glasskube.operator.generic.condition.DeploymentReadyCondition import eu.glasskube.utils.addTo import io.fabric8.kubernetes.api.model.HTTPGetAction import io.fabric8.kubernetes.api.model.Probe @@ -52,6 +53,7 @@ class GiteaDeployment(private val configService: ConfigService) : CRUDKubernetesDependentResource(Deployment::class.java) { internal class Discriminator : ResourceIDMatcherDiscriminator({ ResourceID(it.deploymentName, it.namespace) }) + internal class ReadyCondition : DeploymentReadyCondition() override fun desired(primary: Gitea, context: Context) = deployment { metadata { diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaIniConfigMap.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaIniConfigMap.kt index faf6f573..b801eb17 100644 --- a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaIniConfigMap.kt +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/dependent/GiteaIniConfigMap.kt @@ -34,7 +34,7 @@ class GiteaIniConfigMap : CRUDKubernetesDependentResource(Conf namespace(primary.namespace) labels(primary.resourceLabels) } - data = primary.baseConfig + getSmtpConfig(primary, context) + data = primary.baseConfig + getSmtpConfig(primary, context) + primary.actionsConfig } private val Gitea.baseConfig: Map @@ -87,6 +87,11 @@ class GiteaIniConfigMap : CRUDKubernetesDependentResource(Conf } } + private val Gitea.actionsConfig: Map + get() = mapOf( + "GITEA__actions__ENABLED" to spec.actions.enabled.toString() + ) + override fun onUpdated(primary: Gitea, updated: ConfigMap, actual: ConfigMap, context: Context) { super.onUpdated(primary, updated, actual, context) context.getSecondaryResource(GiteaDeployment.Discriminator()).ifPresent { diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunner.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunner.kt new file mode 100644 index 00000000..4248eb2a --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunner.kt @@ -0,0 +1,34 @@ +package eu.glasskube.operator.apps.gitea.runner + +import eu.glasskube.operator.Labels +import eu.glasskube.operator.apps.gitea.Gitea +import io.fabric8.kubernetes.api.model.Namespaced +import io.fabric8.kubernetes.client.CustomResource +import io.fabric8.kubernetes.model.annotation.Group +import io.fabric8.kubernetes.model.annotation.Version + +@Group("glasskube.eu") +@Version("v1alpha1") +class GiteaActionRunner : CustomResource(), Namespaced { + companion object { + internal const val APP_NAME = "act-runner" + internal const val APP_VERSION = "0.2.6" + internal const val APP_IMAGE = "${Gitea.APP_NAME}/act_runner:$APP_VERSION" + internal const val DOCKER_IMAGE = "docker:23.0.6-dind" + } +} + +fun giteaActionRunner(block: GiteaActionRunner.() -> Unit) = GiteaActionRunner().apply(block) + +internal val GiteaActionRunner.resourceLabels + get() = Labels.resourceLabels( + GiteaActionRunner.APP_NAME, + metadata.name, + Gitea.APP_NAME, + GiteaActionRunner.APP_VERSION, + GiteaActionRunner.APP_NAME + ) +internal val GiteaActionRunner.resourceLabelSelector + get() = Labels.resourceLabelSelector(GiteaActionRunner.APP_NAME, metadata.name, Gitea.APP_NAME) +internal val GiteaActionRunner.genericResourceName get() = "${GiteaActionRunner.APP_NAME}-${metadata.name}" +internal val GiteaActionRunner.secretName get() = "$genericResourceName-token" diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerReconciler.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerReconciler.kt new file mode 100644 index 00000000..35fcbe98 --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerReconciler.kt @@ -0,0 +1,36 @@ +package eu.glasskube.operator.apps.gitea.runner + +import eu.glasskube.kubernetes.client.patchOrUpdateStatus +import eu.glasskube.operator.Labels +import eu.glasskube.operator.api.reconciler.getSecondaryResource +import eu.glasskube.operator.apps.gitea.Gitea +import eu.glasskube.operator.apps.gitea.runner.dependent.GiteaActionRunnerSecret +import eu.glasskube.operator.apps.gitea.runner.dependent.GiteaActionRunnerStatefulSet +import eu.glasskube.operator.generic.condition.isReady +import io.fabric8.kubernetes.api.model.apps.StatefulSet +import io.javaoperatorsdk.operator.api.reconciler.Context +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration +import io.javaoperatorsdk.operator.api.reconciler.Reconciler +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent +import kotlin.jvm.optionals.getOrDefault + +@ControllerConfiguration( + dependents = [ + Dependent(type = GiteaActionRunnerStatefulSet::class), + Dependent(type = GiteaActionRunnerSecret::class) + ] +) +class GiteaActionRunnerReconciler : Reconciler { + override fun reconcile(resource: GiteaActionRunner, context: Context) = with(context) { + resource.patchOrUpdateStatus( + GiteaActionRunnerStatus( + getSecondaryResource().map { it.isReady }.getOrDefault(false) + ) + ) + } + + companion object { + internal const val SELECTOR = + "${Labels.MANAGED_BY_GLASSKUBE},${Labels.PART_OF}=${Gitea.APP_NAME},${Labels.NAME}=${GiteaActionRunner.APP_NAME}" + } +} diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerSpec.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerSpec.kt new file mode 100644 index 00000000..33fa7144 --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerSpec.kt @@ -0,0 +1,11 @@ +package eu.glasskube.operator.apps.gitea.runner + +import io.fabric8.generator.annotation.Required +import io.fabric8.kubernetes.api.model.LocalObjectReference + +data class GiteaActionRunnerSpec( + @field:Required + val token: String, + @field:Required + val gitea: LocalObjectReference +) diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerStatus.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerStatus.kt new file mode 100644 index 00000000..918e921d --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/GiteaActionRunnerStatus.kt @@ -0,0 +1,5 @@ +package eu.glasskube.operator.apps.gitea.runner + +data class GiteaActionRunnerStatus( + val ready: Boolean +) diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/dependent/GiteaActionRunnerSecret.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/dependent/GiteaActionRunnerSecret.kt new file mode 100644 index 00000000..5eb2a2d2 --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/dependent/GiteaActionRunnerSecret.kt @@ -0,0 +1,26 @@ +package eu.glasskube.operator.apps.gitea.runner.dependent + +import eu.glasskube.kubernetes.api.model.metadata +import eu.glasskube.kubernetes.api.model.namespace +import eu.glasskube.kubernetes.api.model.secret +import eu.glasskube.operator.apps.gitea.runner.GiteaActionRunner +import eu.glasskube.operator.apps.gitea.runner.GiteaActionRunnerReconciler +import eu.glasskube.operator.apps.gitea.runner.resourceLabels +import eu.glasskube.operator.apps.gitea.runner.secretName +import eu.glasskube.utils.encodeBase64 +import io.fabric8.kubernetes.api.model.Secret +import io.javaoperatorsdk.operator.api.reconciler.Context +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent + +@KubernetesDependent(labelSelector = GiteaActionRunnerReconciler.SELECTOR) +class GiteaActionRunnerSecret : CRUDKubernetesDependentResource(Secret::class.java) { + override fun desired(primary: GiteaActionRunner, context: Context) = secret { + metadata { + name(primary.secretName) + namespace(primary.namespace) + labels(primary.resourceLabels) + } + data = mapOf("GITEA_RUNNER_REGISTRATION_TOKEN" to primary.spec.token.encodeBase64()) + } +} diff --git a/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/dependent/GiteaActionRunnerStatefulSet.kt b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/dependent/GiteaActionRunnerStatefulSet.kt new file mode 100644 index 00000000..79eb4b2f --- /dev/null +++ b/operator/src/main/kotlin/eu/glasskube/operator/apps/gitea/runner/dependent/GiteaActionRunnerStatefulSet.kt @@ -0,0 +1,140 @@ +package eu.glasskube.operator.apps.gitea.runner.dependent + +import eu.glasskube.kubernetes.api.model.apps.statefulSet +import eu.glasskube.kubernetes.api.model.container +import eu.glasskube.kubernetes.api.model.emptyDir +import eu.glasskube.kubernetes.api.model.env +import eu.glasskube.kubernetes.api.model.envFrom +import eu.glasskube.kubernetes.api.model.envVar +import eu.glasskube.kubernetes.api.model.limits +import eu.glasskube.kubernetes.api.model.metadata +import eu.glasskube.kubernetes.api.model.namespace +import eu.glasskube.kubernetes.api.model.requests +import eu.glasskube.kubernetes.api.model.resources +import eu.glasskube.kubernetes.api.model.secretRef +import eu.glasskube.kubernetes.api.model.securityContext +import eu.glasskube.kubernetes.api.model.spec +import eu.glasskube.kubernetes.api.model.volume +import eu.glasskube.kubernetes.api.model.volumeMount +import eu.glasskube.kubernetes.api.model.volumeMounts +import eu.glasskube.operator.apps.gitea.runner.GiteaActionRunner +import eu.glasskube.operator.apps.gitea.runner.GiteaActionRunnerReconciler +import eu.glasskube.operator.apps.gitea.runner.genericResourceName +import eu.glasskube.operator.apps.gitea.runner.resourceLabelSelector +import eu.glasskube.operator.apps.gitea.runner.resourceLabels +import eu.glasskube.operator.apps.gitea.runner.secretName +import io.fabric8.kubernetes.api.model.Quantity +import io.fabric8.kubernetes.api.model.apps.StatefulSet +import io.javaoperatorsdk.operator.api.reconciler.Context +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent + +@KubernetesDependent(labelSelector = GiteaActionRunnerReconciler.SELECTOR) +class GiteaActionRunnerStatefulSet : + CRUDNoGCKubernetesDependentResource(StatefulSet::class.java) { + override fun desired(primary: GiteaActionRunner, context: Context) = statefulSet { + metadata { + name(primary.genericResourceName) + namespace(primary.namespace) + labels(primary.resourceLabels) + } + spec { + selector { matchLabels = primary.resourceLabelSelector } + replicas(1) + updateStrategyRollingUpdate { + maxUnavailable("100%") + } + volumeClaimTemplates { + volumeClaimTemplate { + metadata { name(RUNNER_DATA_VOLUME) } + spec { + resources { requests = mapOf("storage" to Quantity("5", "Gi")) } + accessModes = listOf("ReadWriteOnce") + } + } + } + template { + metadata { labels(primary.resourceLabels) } + spec { + volumes = listOf( + volume(DOCKER_CERT_VOLUME) { emptyDir() } + ) + containers = listOf( + container { + name = GiteaActionRunner.APP_NAME + image = GiteaActionRunner.APP_IMAGE + command = listOf("sh") + args = listOf( + "-c", + "while ! nc -z localhost 2376 : Condition { + override fun isMet(dependentResource: DependentResource, primary: T, context: Context) = + dependentResource.getSecondaryResource(primary, context) + .map { it.isReady } + .getOrDefault(false) +} + +enum class StatefulSetState { + DOWN, DEGRADED, RUNNING +} + +val StatefulSet.state + get() = when (status?.readyReplicas) { + null, 0 -> StatefulSetState.DOWN + spec.replicas -> StatefulSetState.RUNNING + else -> StatefulSetState.DEGRADED + } + +val StatefulSet.isReady + get() = state == StatefulSetState.RUNNING