Skip to content

Commit

Permalink
Fix reaping which was broken in 0.4
Browse files Browse the repository at this point in the history
- create limit ranges and resource quotas for new namespaces
- clean up logging so it shows sensible info
- add code to remove deleted namespaces
  • Loading branch information
sgdan committed Jan 1, 2020
1 parent d3a854f commit 3b4457d
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 22 deletions.
8 changes: 7 additions & 1 deletion src/main/kotlin/org/sgdan/podreaper/Actions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ fun saveSettings(client: KubernetesClient, settings: Map<String, NamespaceConfig
}

fun reap(client: KubernetesClient, status: Status, ns: NamespaceStatus) {
if (!ns.hasLimitRange) createLimitRange(client, ns.name)
if (!ns.hasResourceQuota) createResourceQuota(client, ns.name, RESOURCE_QUOTA_NAME, DEFAULT_QUOTA)

val started = mostRecent(ns.lastScheduled, toZDT(ns.lastStarted, status.zone))
val shouldRun = hoursFrom(started, status.zdt) < 8

Expand All @@ -57,7 +60,10 @@ fun reap(client: KubernetesClient, status: Status, ns: NamespaceStatus) {
}

// kill any pods that are running
if (!shouldRun) client.pods().inNamespace(ns.name).delete()
if (!shouldRun) client.pods().inNamespace(ns.name).let {
val n = it.list().items.size
if (n > 0 && it.delete()) log.info { "Deleted $n pods in ${ns.name}" }
}
}

fun createLimitRange(client: KubernetesClient, ns: String) {
Expand Down
19 changes: 14 additions & 5 deletions src/main/kotlin/org/sgdan/podreaper/Backend.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class Backend(private val client: KubernetesClient,
try {
current = updateNamespaces(current.copy(now = currentTimeMillis()), client, cfg.force)
} catch (e: Exception) {
log.error(e) { "Unable to update" }
log.error { "Unable to update: ${e.message}" }
}
}

Expand All @@ -66,7 +66,7 @@ class Backend(private val client: KubernetesClient,
try {
current = reapNamespaces(current, client)
} catch (e: Exception) {
log.error(e) { "Unable to reap" }
log.error { "Unable to reap: ${e.message}" }
}
}

Expand All @@ -78,22 +78,31 @@ class Backend(private val client: KubernetesClient,
try {
setMemLimit(current, client, namespace, limit).also { current = it }
} catch (e: Exception) {
current.copy(error = "Unable to set start hour for $namespace: ${e.message}")
"Unable to set start hour for $namespace: ${e.message}".let {
log.error { it }
current.copy(error = it)
}
}

@Synchronized
fun setStartHour(namespace: String, autoStartHour: Int?): Status =
try {
setStartHour(current, client, namespace, autoStartHour).also { current = it }
} catch (e: Exception) {
current.copy(error = "Unable to set start hour for $namespace: ${e.message}")
"Unable to set start hour for $namespace: ${e.message}".let {
log.error { it }
current.copy(error = it)
}
}

@Synchronized
fun extend(namespace: String): Status =
try {
extend(current, client, namespace).also { current = it }
} catch (e: Exception) {
current.copy(error = "Unable to extend namespace: ${e.message}")
"Unable to extend namespace: ${e.message}".let {
log.error { it }
current.copy(error = it)
}
}
}
38 changes: 29 additions & 9 deletions src/main/kotlin/org/sgdan/podreaper/Operations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,29 +33,49 @@ fun extend(current: Status, client: KubernetesClient, name: String): Status {
return loadNamespace(current.copy(settings = newSettings), client, name)
}

/** Update 20% of namespaces with each tick */
/**
* Decide how many namespaces to process in this tick. If there are many,
* just do 20% at a time.
*/
fun numToTake(size: Int) = if (size > 25) size / 5 else 5

fun updateNamespaces(current: Status, client: KubernetesClient, force: Boolean = false): Status {
val allNames = client.namespaces().list().items
val live = client.namespaces().list().items
.map { it.metadata.name }
.filter { !current.ignoredNamespaces.contains(it) }
val loadedNames = current.namespaces.map { it.name }
val toLoad = allNames.minus(loadedNames)
val toUpdate = current.namespaces
val loaded = current.namespaces.map { it.name }

// remove namespaces that have been deleted
val toRemove = loaded.minus(live)
val afterRemove = current.copy(namespaces = current.namespaces.filter {
!toRemove.contains(it.name)
}, settings = current.settings.filter {
!toRemove.contains(it.key)
})
if (toRemove.isNotEmpty()) {
log.info { "Removed namespaces: $toRemove" }
saveSettings(client, afterRemove.settings)
}

// figure out which need to be loaded and/or updated
val toLoad = live.minus(loaded)
val toUpdate = afterRemove.namespaces
.sortedBy { it.lastRefreshed }
.take(current.namespaces.size / 5)
.take(numToTake(current.namespaces.size))
.map { it.name }
return allNames.fold(current, { result, name ->

// load and/or update
return live.fold(afterRemove, { result, name ->
if (force || toUpdate.contains(name) || toLoad.contains(name))
loadNamespace(result, client, name)
else result
})
}

/** Reap 20% of namespaces with each tick */
fun reapNamespaces(current: Status, client: KubernetesClient): Status =
current.namespaces
.sortedBy { -it.lastRefreshed }
.take(current.namespaces.size / 5)
.take(numToTake(current.namespaces.size))
.fold(current, { result, namespace ->
reap(client, result, namespace)
loadNamespace(result, client, namespace.name)
Expand Down
10 changes: 3 additions & 7 deletions src/main/kotlin/org/sgdan/podreaper/Status.kt
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,12 @@ fun readResourceQuota(client: KubernetesClient, namespace: String): ResourceQuot
return client.resourceQuotas().inNamespace(namespace).withName(RESOURCE_QUOTA_NAME).get()
}

fun defaultLimitRangeItem(): LimitRangeItem = LimitRangeItemBuilder()
.withDefault(mapOf(MEMORY to Quantity(POD_LIMIT)))
.withDefaultRequest(mapOf(MEMORY to Quantity(POD_REQUEST)))
.build()!!

fun hasLimitRange(client: KubernetesClient, namespace: String): Boolean =
client.limitRanges().inNamespace(namespace)
.withName(LIMIT_RANGE_NAME)
.get()?.let {
it.spec?.limits?.contains(defaultLimitRangeItem())
.get()?.spec?.limits?.get(0)?.let {
it.default?.get(MEMORY)?.amount == POD_LIMIT
&& it.defaultRequest?.get(MEMORY)?.amount == POD_REQUEST
} ?: false

operator fun Regex.contains(text: CharSequence): Boolean = matches(text)
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.sgdan.podreaper" level="INFO"/>
</configuration>

0 comments on commit 3b4457d

Please sign in to comment.