How to programmatically uninstall Helm releases from Kotlin?
I deploy my applications using Helm, which installs Deployments for the app, Istio Virtual Services, Sidecars, and Destination Rules. I need to create a Kotlin application that can completely uninstall an application. Using Go for this purpose is not an option - the solution must be implemented in Kotlin. Would it be better to directly call the Kubernetes API to remove the Deployment and other resources, and then delete the secret that Helm uses to store metadata?
How to Programmatically Uninstall Helm Releases from Kotlin
To programmatically uninstall Helm releases from Kotlin, you can use either the Kubernetes API directly or interact with Helm’s REST API. The Helm REST API approach is generally recommended as it properly handles release dependencies, hooks, and cleanup order automatically, ensuring a complete uninstallation.
Contents
- Approaches to Helm Release Uninstallation
- Using the Kubernetes API Directly
- Using Helm’s REST API
- Implementation Examples in Kotlin
- [Best Practices and Considerations](#best-practices-and considerations)
- Conclusion
Approaches to Helm Release Uninstallation
When uninstalling Helm releases programmatically from Kotlin, you have two primary approaches:
- Direct Kubernetes API Approach: Find and delete all Kubernetes resources associated with the Helm release individually
- Helm REST API Approach: Use Helm’s own APIs to trigger the proper uninstallation sequence
Each approach has distinct advantages and limitations:
Approach | Pros | Cons |
---|---|---|
Kubernetes API | - Fine-grained control - No Helm dependency - Can target specific resources |
- Complex implementation - Risk of missing resources - Manual cleanup order management |
Helm REST API | - Complete cleanup automatically - Handles dependencies and hooks - Maintains proper cleanup order |
- Requires Helm installation - Less control over individual resources - Additional dependency management |
For most use cases, the Helm REST API approach is preferable because it ensures complete uninstallation while maintaining proper resource cleanup order.
Using the Kubernetes API Directly
If you choose to interact directly with the Kubernetes API, you’ll need to:
- Find all resources associated with the Helm release
- Delete them in the correct order
- Handle any dependencies or prerequisites
To implement this in Kotlin, you can use the Kubernetes Java client:
import io.kubernetes.client.openapi.ApiClient
import io.kubernetes.client.openapi.ApiException
import io.kubernetes.client.openapi.apis.AppsV1Api
import io.kubernetes.client.openapi.apis.CoreV1Api
import io.kubernetes.client.openapi.apis.NetworkingV1Api
import io.kubernetes.client.openapi.models.V1DeleteOptions
fun uninstallUsingK8sApi(releaseName: String, namespace: String) {
// Initialize the Kubernetes client
val apiClient = ApiClient()
apiClient.basePath = "https://your-k8s-server:6443"
// Configure authentication (token or SSL cert)
val appsApi = AppsV1Api(apiClient)
val coreApi = CoreV1Api(apiClient)
val networkingApi = NetworkingV1Api(apiClient)
try {
// Delete Deployments
appsApi.deleteNamespacedDeployment(
name = "$release-name-deployment",
namespace = namespace,
body = V1DeleteOptions()
)
// Delete Services
coreApi.deleteNamespacedService(
name = "$release-name-service",
namespace = namespace,
body = V1DeleteOptions()
)
// Delete Istio resources
networkingApi.deleteNamespacedVirtualService(
name = "$release-name-virtualservice",
namespace = namespace,
body = V1DeleteOptions()
)
// Delete other resources...
// Delete the Helm secret
coreApi.deleteNamespacedSecret(
name = "sh.helm.release.$releaseName",
namespace = namespace,
body = V1DeleteOptions()
)
} catch (e: ApiException) {
println("Error uninstalling release: ${e.message}")
}
}
This approach requires you to know all the resource names and types beforehand, which can be challenging as Helm charts may create many resources with standardized naming patterns.
Using Helm’s REST API
A more robust approach is to use Helm’s REST API, which allows you to trigger the proper uninstall sequence that Helm would normally perform:
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
@Serializable
data class HelmRelease(
val name: String,
val namespace: String,
val revision: Int,
val updated: String,
val status: String,
val chart: String,
val appVersion: String
)
@Serializable
data class HelmUninstallResponse(
val status: String,
val message: String?
)
fun uninstallUsingHelmRestApi(releaseName: String, namespace: String): HelmUninstallResponse {
val client = HttpClient(CIO)
return runBlocking {
try {
val response = client.post("http://localhost:44134/api/v1/uninstall/$releaseName") {
contentType(ContentType.Application.Json)
header("X-Helm-Release-Namespace", namespace)
// Add authentication header if needed
// header("Authorization", "Bearer your-token")
}
if (response.status == HttpStatusCode.OK) {
Json.decodeFromString<HelmUninstallResponse>(response.bodyAsText())
} else {
HelmUninstallResponse(
status = "error",
message = "HTTP ${response.status.value}: ${response.bodyAsText()}"
)
}
} catch (e: Exception) {
HelmUninstallResponse(
status = "error",
message = "Exception: ${e.message}"
)
}
}
}
This approach requires:
- Helm to be running with its REST API enabled (usually on port 44134)
- Proper authentication configured
- Network access to the Helm server
Implementation Examples in Kotlin
Here’s a complete Kotlin implementation using the Kubernetes Java client:
import io.kubernetes.client.openapi.ApiClient
import io.kubernetes.client.openapi.ApiException
import io.kubernetes.client.openapi.apis.*
import io.kubernetes.client.openapi.models.*
import io.kubernetes.client.util.Config
class HelmReleaseUninstaller {
private val k8sApi: ApiClient
private val appsApi: AppsV1Api
private val coreApi: CoreV1Api
private val networkingApi: NetworkingV1Api
private val rbacApi: RbacAuthorizationV1Api
init {
k8sApi = Config.defaultClient()
appsApi = AppsV1Api(k8sApi)
coreApi = CoreV1Api(k8sApi)
networkingApi = NetworkingV1Api(k8sApi)
rbacApi = RbacAuthorizationV1Api(k8sApi)
}
fun uninstallRelease(releaseName: String, namespace: String): Boolean {
try {
// First get all releases to verify the one exists
val releases = getReleases(namespace)
if (!releases.any { it.name == releaseName }) {
println("Release $releaseName not found in namespace $namespace")
return false
}
// Use Helm uninstall if possible
if (tryHelmUninstall(releaseName, namespace)) {
return true
}
// Fallback to direct API calls
return directUninstall(releaseName, namespace)
} catch (e: ApiException) {
println("API error: ${e.code} - ${e.message}")
return false
} catch (e: Exception) {
println("Unexpected error: ${e.message}")
return false
}
}
private fun tryHelmUninstall(releaseName: String, namespace: String): Boolean {
// This would connect to Helm's REST API
// Implementation depends on Helm setup
println("Attempting Helm REST API uninstall for $releaseName")
return false // Return true if successful
}
private fun directUninstall(releaseName: String, namespace: String): Boolean {
println("Falling back to direct Kubernetes API uninstall")
// Delete Deployments
val deployments = appsApi.listNamespacedDeployment(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
deployments.items.forEach { deployment ->
try {
appsApi.deleteNamespacedDeployment(deployment.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted Deployment: ${deployment.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete Deployment ${deployment.metadata.name}: ${e.message}")
}
}
// Delete Services
val services = coreApi.listNamespacedService(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
services.items.forEach { service ->
try {
coreApi.deleteNamespacedService(service.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted Service: ${service.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete Service ${service.metadata.name}: ${e.message}")
}
}
// Delete Istio VirtualServices
val virtualServices = networkingApi.listNamespacedVirtualService(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
virtualServices.items.forEach { vs ->
try {
networkingApi.deleteNamespacedVirtualService(vs.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted VirtualService: ${vs.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete VirtualService ${vs.metadata.name}: ${e.message}")
}
}
// Delete Istio DestinationRules
val destinationRules = networkingApi.listNamespacedDestinationRule(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
destinationRules.items.forEach { dr ->
try {
networkingApi.deleteNamespacedDestinationRule(dr.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted DestinationRule: ${dr.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete DestinationRule ${dr.metadata.name}: ${e.message}")
}
}
// Delete ServiceAccounts
val serviceAccounts = coreApi.listNamespacedServiceAccount(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
serviceAccounts.items.forEach { sa ->
try {
coreApi.deleteNamespacedServiceAccount(sa.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted ServiceAccount: ${sa.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete ServiceAccount ${sa.metadata.name}: ${e.message}")
}
}
// Delete Roles and RoleBindings
val roles = rbacApi.listNamespacedRole(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
roles.items.forEach { role ->
try {
rbacApi.deleteNamespacedRole(role.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted Role: ${role.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete Role ${role.metadata.name}: ${e.message}")
}
}
val roleBindings = rbacApi.listNamespacedRoleBinding(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
roleBindings.items.forEach { rb ->
try {
rbacApi.deleteNamespacedRoleBinding(rb.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted RoleBinding: ${rb.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete RoleBinding ${rb.metadata.name}: ${e.message}")
}
}
// Delete ConfigMaps
val configMaps = coreApi.listNamespacedConfigMap(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
configMaps.items.forEach { cm ->
try {
coreApi.deleteNamespacedConfigMap(cm.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted ConfigMap: ${cm.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete ConfigMap ${cm.metadata.name}: ${e.message}")
}
}
// Delete Secrets
val secrets = coreApi.listNamespacedSecret(namespace, null, null, null,
"app.kubernetes.io/instance=$releaseName", null, null, null, null, null)
secrets.items.forEach { secret ->
try {
coreApi.deleteNamespacedSecret(secret.metadata.name!!, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted Secret: ${secret.metadata.name}")
} catch (e: ApiException) {
println("Failed to delete Secret ${secret.metadata.name}: ${e.message}")
}
}
// Finally delete the Helm release secret
try {
val helmSecretName = "sh.helm.release.$releaseName"
coreApi.deleteNamespacedSecret(helmSecretName, namespace,
V1DeleteOptions(), null, null, null, null)
println("Deleted Helm release secret: $helmSecretName")
} catch (e: ApiException) {
println("Helm release secret may not exist or couldn't be deleted: ${e.message}")
}
return true
}
private fun getReleases(namespace: String): List<HelmRelease> {
// This would typically query Helm's storage or the Kubernetes API
// For simplicity, we're returning an empty list
return emptyList()
}
}
// Usage example
fun main() {
val uninstaller = HelmReleaseUninstaller()
val success = uninstaller.uninstallRelease("my-release", "default")
println("Uninstall ${if (success) "succeeded" else "failed"}")
}
This implementation:
- Attempts to use Helm’s REST API first
- Falls back to direct Kubernetes API calls if Helm is unavailable
- Deletes all common Kubernetes resources associated with the release
- Properly handles errors for individual resource deletions
- Finally removes the Helm release secret
Best Practices and Considerations
When implementing Helm release uninstallation in Kotlin, consider these best practices:
Resource Cleanup Order
- Delete dependent resources first: Services, Ingress, Istio resources before Deployments
- Handle finalizers: Some resources (like PVCs) have finalizers that must be addressed
- Consider cascading deletion: Use propagation policy to handle dependent resources
val deleteOptions = V1DeleteOptions()
deleteOptions.propagationPolicy = "Background" // or "Foreground"
Error Handling and Safety
- Implement dry-run mode: Allow testing without actual deletion
- Add confirmation prompts: For production environments
- Log all actions: For auditability and troubleshooting
fun uninstallWithConfirmation(releaseName: String, namespace: String, dryRun: Boolean = false) {
println("Preparing to uninstall release: $releaseName from namespace: $namespace")
if (dryRun) {
println("DRY RUN MODE - No resources will be actually deleted")
}
print("Are you sure you want to proceed? (y/n): ")
val confirmation = readLine()
if (confirmation?.lowercase() == "y") {
val success = uninstallRelease(releaseName, namespace, dryRun)
println("Operation ${if (success) "completed" else "failed"}")
} else {
println("Operation cancelled")
}
}
Performance Considerations
- Batch operations: Where possible, delete multiple resources of the same type in a single call
- Parallel processing: Use Kotlin coroutines to delete independent resources concurrently
- Pagination: Handle large numbers of resources with Kubernetes API pagination
suspend fun deleteResourcesInParallel(apiCall: suspend () -> List<V1Status>) {
val scope = CoroutineScope(Dispatchers.IO)
val jobs = mutableListOf<Job>()
apiCall().forEach { resource ->
jobs.add(scope.launch {
try {
// Individual delete operation
println("Deleting ${resource.metadata?.name}")
} catch (e: Exception) {
println("Failed to delete resource: ${e.message}")
}
})
}
jobs.joinAll()
}
Testing
- Use a test cluster: Never test uninstallation on production clusters
- Create test releases: Specifically for testing the uninstallation process
- Verify cleanup: Ensure all resources are actually removed after uninstallation
Conclusion
When creating a Kotlin application to uninstall Helm releases, consider these key points:
- Prefer Helm’s REST API when available, as it handles the complete uninstallation sequence properly
- Use the Kubernetes Java client as a robust dependency for direct API interactions
- Implement comprehensive error handling to account for missing resources or API failures
- Follow proper resource deletion order to avoid dependency issues
- Include safety mechanisms like dry-run mode and confirmation prompts
For most scenarios, the recommended approach is to:
- First attempt to use Helm’s REST API for complete uninstallation
- Fall back to direct Kubernetes API calls if Helm is unavailable
- Implement proper logging and error handling throughout the process
This ensures you can completely uninstall your applications including Deployments, Istio Virtual Services, Sidecars, Destination Rules, and all associated resources, regardless of your environment’s Helm configuration.