GLM 4.5 Air

Complete Guide: Uninstall Helm Releases in Kotlin

Learn how to programmatically uninstall Helm releases from Kotlin using Kubernetes API or Helm REST API. Complete implementation examples and best practices included.

Question

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?

GLM 4.5 Air

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

When uninstalling Helm releases programmatically from Kotlin, you have two primary approaches:

  1. Direct Kubernetes API Approach: Find and delete all Kubernetes resources associated with the Helm release individually
  2. 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:

  1. Find all resources associated with the Helm release
  2. Delete them in the correct order
  3. Handle any dependencies or prerequisites

To implement this in Kotlin, you can use the Kubernetes Java client:

kotlin
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:

kotlin
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:

  1. Helm to be running with its REST API enabled (usually on port 44134)
  2. Proper authentication configured
  3. Network access to the Helm server

Implementation Examples in Kotlin

Here’s a complete Kotlin implementation using the Kubernetes Java client:

kotlin
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:

  1. Attempts to use Helm’s REST API first
  2. Falls back to direct Kubernetes API calls if Helm is unavailable
  3. Deletes all common Kubernetes resources associated with the release
  4. Properly handles errors for individual resource deletions
  5. Finally removes the Helm release secret

Best Practices and Considerations

When implementing Helm release uninstallation in Kotlin, consider these best practices:

Resource Cleanup Order

  1. Delete dependent resources first: Services, Ingress, Istio resources before Deployments
  2. Handle finalizers: Some resources (like PVCs) have finalizers that must be addressed
  3. Consider cascading deletion: Use propagation policy to handle dependent resources
kotlin
val deleteOptions = V1DeleteOptions()
deleteOptions.propagationPolicy = "Background" // or "Foreground"

Error Handling and Safety

  1. Implement dry-run mode: Allow testing without actual deletion
  2. Add confirmation prompts: For production environments
  3. Log all actions: For auditability and troubleshooting
kotlin
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

  1. Batch operations: Where possible, delete multiple resources of the same type in a single call
  2. Parallel processing: Use Kotlin coroutines to delete independent resources concurrently
  3. Pagination: Handle large numbers of resources with Kubernetes API pagination
kotlin
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

  1. Use a test cluster: Never test uninstallation on production clusters
  2. Create test releases: Specifically for testing the uninstallation process
  3. Verify cleanup: Ensure all resources are actually removed after uninstallation

Conclusion

When creating a Kotlin application to uninstall Helm releases, consider these key points:

  1. Prefer Helm’s REST API when available, as it handles the complete uninstallation sequence properly
  2. Use the Kubernetes Java client as a robust dependency for direct API interactions
  3. Implement comprehensive error handling to account for missing resources or API failures
  4. Follow proper resource deletion order to avoid dependency issues
  5. Include safety mechanisms like dry-run mode and confirmation prompts

For most scenarios, the recommended approach is to:

  1. First attempt to use Helm’s REST API for complete uninstallation
  2. Fall back to direct Kubernetes API calls if Helm is unavailable
  3. 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.