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

Feature schedule initial risc generation #262

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
9 changes: 8 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
ISSUER_URI="http://localhost:7007/api/auth"
RISC_FOLDER_PATH=".security/risc"
PATH_REGEX="\.risc\.yaml$"
FILENAME_PREFIX="risc"
FILENAME_POSTFIX="risc"
FILENAME_POSTFIX="risc"
CRYPTO_SERVICE_URL="http://localhost:8084"
REDIS_HOSTNAME="localhost"
KUBERNETES_NAMESPACE="" # Not applicable as initialize RiSc is not possible local
INITIALIZE_RISC_IMAGE_URL="" # Not applicable as initialize RiSc is not possible local
INIT_RISC_EXTERNAL_SECRETS_NAME="" # Not applicable as initialize RiSc is not possible local
APP_NAME="" # Not applicable as initialize RiSc is not possible local
6 changes: 1 addition & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,4 @@ build/
http-client.private.env.json
.DS_Store
.security/.DS_Store
*.private.env.jsons


# Env
.env.local
*.private.env.jsons
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.data:spring-data-redis")
implementation("org.springframework.security:spring-security-oauth2-jose")
implementation("org.springframework.security:spring-security-oauth2-resource-server")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")

implementation("redis.clients:jedis")

implementation("io.netty:netty-all:4.1.112.Final")

implementation("io.micrometer:micrometer-registry-prometheus")
Expand All @@ -52,6 +55,8 @@ dependencies {
implementation("com.nimbusds:nimbus-jose-jwt:9.40")
implementation("org.bouncycastle:bcpkix-jdk18on:1.78.1")

implementation("io.kubernetes:client-java:21.0.1")

implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
Expand All @@ -60,6 +65,9 @@ dependencies {
testImplementation("com.ninja-squad:springmockk:4.0.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")

// Development
developmentOnly("org.springframework.boot:spring-boot-docker-compose")
}

tasks.withType<KotlinCompile> {
Expand Down
35 changes: 9 additions & 26 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,12 @@
services:
local:
build:
context: .
dockerfile: Dockerfile
cache:
image: redis:alpine
restart: always
container_name: "ros-backend-redis"
ports:
- "8080:8080"
- "8081:8081"
- '6379:6379'
volumes:
- .:/code
env_file:
- .env
skip:
build:
context: .
dockerfile: Dockerfile
environment:
- ISSUER_URI=${ISSUER_URI}
- GITHUB_APP_ID=${GITHUB_APP_ID}
- GITHUB_INSTALLATION_ID=${GITHUB_INSTALLATION_ID}
- GITHUB_PRIVATE_KEY_BASE64_ENCODED=${GITHUB_PRIVATE_KEY_BASE64_ENCODED}
- RISC_FOLDER_PATH=${RISC_FOLDER_PATH}
- FILENAME_PREFIX=${FILENAME_PREFIX}
- FILENAME_POSTFIX=${FILENAME_POSTFIX}
ports:
- "8080:8080"
- "8081:8081"
volumes:
- .:/code
- cache:/data
volumes:
cache:
driver: local
25 changes: 25 additions & 0 deletions src/main/kotlin/no/risc/config/RedisConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package no.risc.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory
import kotlin.properties.Delegates

@Configuration
@ConfigurationProperties(prefix = "redis")
class RedisConfig {
lateinit var hostname: String
var port by Delegates.notNull<Int>()

@Bean
fun jedisConnectionFactory(redisConfig: RedisConfig): JedisConnectionFactory {
val redisClientConfig =
RedisStandaloneConfiguration().apply {
this.hostName = redisConfig.hostname
this.port = redisConfig.port
}
return JedisConnectionFactory(redisClientConfig)
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/no/risc/config/SkiperatorConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package no.risc.config

import io.kubernetes.client.openapi.apis.CustomObjectsApi
import io.kubernetes.client.util.Config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration
@ConfigurationProperties(prefix = "skiperator")
class SkiperatorConfig {
lateinit var namespace: String
lateinit var imageUrl: String
lateinit var externalSecretsName: String
lateinit var riScBackendApplicationName: String

fun customObjectsApi() = CustomObjectsApi(apiClient())

fun apiClient() = Config.fromCluster()
}
23 changes: 23 additions & 0 deletions src/main/kotlin/no/risc/exception/GlobalExceptionHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package no.risc.exception
import no.risc.exception.exceptions.AccessTokenValidationFailedException
import no.risc.exception.exceptions.CreatePullRequestException
import no.risc.exception.exceptions.CreatingRiScException
import no.risc.exception.exceptions.InitializeRiScSessionNotFoundException
import no.risc.exception.exceptions.InvalidAccessTokensException
import no.risc.exception.exceptions.JSONSchemaFetchException
import no.risc.exception.exceptions.PermissionDeniedOnGitHubException
import no.risc.exception.exceptions.RepositoryAccessException
import no.risc.exception.exceptions.RiScNotValidOnFetchException
import no.risc.exception.exceptions.RiScNotValidOnUpdateException
import no.risc.exception.exceptions.SOPSDecryptionException
import no.risc.exception.exceptions.ScheduleInitialRiScDuringLocalException
import no.risc.exception.exceptions.ScheduleInitialRiScException
import no.risc.exception.exceptions.SopsConfigFetchException
import no.risc.exception.exceptions.SopsEncryptionException
import no.risc.exception.exceptions.UnableToParseResponseBodyException
Expand All @@ -19,6 +22,7 @@ import no.risc.risc.DecryptionFailure
import no.risc.risc.ProcessRiScResultDTO
import no.risc.risc.ProcessingStatus
import no.risc.risc.RiScContentResultDTO
import no.risc.risc.models.ScheduleInitialRiScDTO
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
Expand Down Expand Up @@ -156,6 +160,19 @@ internal class GlobalExceptionHandler {
)
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(InitializeRiScSessionNotFoundException::class)
fun handleInitializeRiScSessionNotFoundException(ex: InitializeRiScSessionNotFoundException) {
logger.error(ex.message, ex)
}

@ResponseStatus(HttpStatus.NOT_IMPLEMENTED)
@ExceptionHandler(ScheduleInitialRiScDuringLocalException::class)
fun handleScheduleInitialRiScDuringLocalException(ex: ScheduleInitialRiScDuringLocalException): ScheduleInitialRiScDTO {
logger.error(ex.message)
return ex.response
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
@ExceptionHandler(UnableToParseResponseBodyException::class)
Expand All @@ -180,6 +197,12 @@ internal class GlobalExceptionHandler {
}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(ScheduleInitialRiScException::class)
fun handleScheduleInitialRiScException(ex: ScheduleInitialRiScException): ScheduleInitialRiScDTO {
logger.error(ex.message, ex)
return ex.response
}

@ResponseBody
@ExceptionHandler(AccessTokenValidationFailedException::class)
fun handleAccessTokenValidationFailedException(ex: AccessTokenValidationFailedException): Any {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package no.risc.exception.exceptions

data class InitializeRiScSessionNotFoundException(
override val message: String,
) : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package no.risc.exception.exceptions

import no.risc.risc.ProcessingStatus
import no.risc.risc.models.ScheduleInitialRiScDTO

data class ScheduleInitialRiScDuringLocalException(
override val message: String,
val response: ScheduleInitialRiScDTO =
ScheduleInitialRiScDTO(
ProcessingStatus.ErrorWhenSchedulingInitialRiSc,
"Schedule initial RiSc is not supported during local run",
),
) : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package no.risc.exception.exceptions

import no.risc.risc.ProcessingStatus
import no.risc.risc.models.ScheduleInitialRiScDTO

data class ScheduleInitialRiScException(
override val message: String,
val response: ScheduleInitialRiScDTO =
ScheduleInitialRiScDTO(
ProcessingStatus.ErrorWhenSchedulingInitialRiSc,
"Failed to schedule initial RiSc generation",
),
) : Exception()
62 changes: 55 additions & 7 deletions src/main/kotlin/no/risc/github/GithubConnector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ data class RiScApprovalPRStatus(
val hasClosedPr: Boolean,
)

data class CommitMessages(
val riSc: String,
val sopsConfig: String?,
)

@Component
class GithubConnector(
@Value("\${filename.postfix}") private val filenamePostfix: String,
Expand All @@ -111,7 +116,6 @@ class GithubConnector(
): GithubContentResponse {
val sopsConfig =
try {
println("Trying to get sops config from branch: $riScId")
getGithubResponse(
"${githubHelper.uriToFindSopsConfig(owner, repository)}?ref=$riScId",
githubAccessToken.value,
Expand All @@ -120,7 +124,6 @@ class GithubConnector(
?.content
?.decodeBase64()
} catch (e: WebClientResponseException.NotFound) {
println("Trying to get sops config from default branch")
getGithubResponse(
githubHelper.uriToFindSopsConfig(owner, repository),
githubAccessToken.value,
Expand Down Expand Up @@ -284,6 +287,7 @@ class GithubConnector(
riScId: String,
defaultBranch: String,
fileContent: String,
sopsConfig: String? = null,
requiresNewApproval: Boolean,
accessTokens: AccessTokens,
userInfo: UserInfo,
Expand All @@ -292,7 +296,7 @@ class GithubConnector(
val githubAuthor = Author(userInfo.name, userInfo.email, Date.from(Instant.now()))
// Attempt to get SHA for the existing draft
var latestShaForDraft = getSHAForExistingRiScDraftOrNull(owner, repository, riScId, accessToken)
var latestShaForPublished: String? = ""
val latestShaForPublished: String?

// Determine if a new branch is needed. "requires new approval" is used to determine if new PR can be created
// through updating.
Expand All @@ -305,19 +309,63 @@ class GithubConnector(
// Fetch to determine if update or create
latestShaForPublished = getSHAForPublishedRiScOrNull(owner, repository, riScId, accessToken)
if (latestShaForPublished != null) {
"Update RiSc with id: $riScId" + if (requiresNewApproval) " requires new approval" else ""
CommitMessages(
riSc = "Update RiSc with id: $riScId" + if (requiresNewApproval)" requires new approval" else "",
sopsConfig =
if (sopsConfig != null) {
"Update SOPS configuration" + if (requiresNewApproval)" requires new approval" else ""
} else {
null
},
)
} else {
"Create new RiSc with id: $riScId" + if (requiresNewApproval) " requires new approval" else ""
CommitMessages(
riSc = "Create new RiSc with id: $riScId" + if (requiresNewApproval)" requires new approval" else "",
sopsConfig =
if (sopsConfig != null) {
"Create SOPS configuration" + if (requiresNewApproval)" requires new approval" else ""
} else {
null
},
)
}
} else {
"Update RiSc with id: $riScId" + if (requiresNewApproval) " requires new approval" else ""
CommitMessages(
riSc = "Update RiSc with id: $riScId" + if (requiresNewApproval)" requires new approval" else "",
sopsConfig =
if (sopsConfig != null) {
"Update SOPS configuration" + if (requiresNewApproval)" requires new approval" else ""
} else {
null
},
)
}

// Write new sops config if sops config is passed to the method
sopsConfig?.let { config ->
putFileRequestToGithub(
githubHelper.uriToPutSopsConfigOnDraftBranch(owner, repository, riScId),
accessToken,
GithubWriteToFilePayload(
message =
commitMessage.sopsConfig
?: throw IllegalStateException(
"Commit message for SOPS config cannot be " +
"null when method argument 'sopsConfig' is not null",
),
content = config.encodeBase64(),
sha = latestShaForDraft,
branchName = riScId,
author = githubAuthor,
),
)
}

putFileRequestToGithub(
githubHelper.uriToPutRiScOnDraftBranch(owner, repository, riScId),
accessToken,
GithubWriteToFilePayload(
message = commitMessage,
message = commitMessage.riSc,
content = fileContent.encodeBase64(),
sha = latestShaForDraft,
branchName = riScId,
Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/no/risc/github/GithubHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@ class GithubHelper(
draftBranch: String = riScId,
): String = "/$owner/$repository/contents/$riScFolderPath/$riScId.$filenamePostfix.yaml?ref=$draftBranch"

fun uriToPutSopsConfigOnDraftBranch(
owner: String,
repository: String,
riScId: String,
draftBranch: String = riScId,
): String = "/$owner/$repository/contents/$riScFolderPath/.sops.yaml?ref=$draftBranch"

fun uriToGetCommitStatus(
owner: String,
repository: String,
Expand Down
Loading
Loading