postdeploy-hooks/k8s-cleanup/kubectl.go (128 lines of code) (raw):

package main import ( "fmt" "os" "slices" "strings" ) const ( cloudDeployPrefix = "deploy.cloud.google.com/" releaseEnvKey = "CLOUD_DEPLOY_RELEASE" projectEnvKey = "CLOUD_DEPLOY_PROJECT_ID" locationEnvKey = "CLOUD_DEPLOY_LOCATION" pipelineEnvKey = "CLOUD_DEPLOY_DELIVERY_PIPELINE" targetEnvKey = "CLOUD_DEPLOY_TARGET" outputFlag = "-o" nameArg = "name" ) // resourcesToDelete returns a list of resources that are not in the current set of resources // (i.e. the set of resources that were just deployed by Cloud Deploy in the most recent release). func (ce CommandExecutor) resourcesToDelete(namespace, resourceTypeFlag string) ([]string, error) { // Step 1. Get a list of resource types to query. resourceTypes, err := ce.resourceTypesToQuery(resourceTypeFlag) if err != nil { return nil, fmt.Errorf("failed to get a list of resources types to query, err: %w", err) } // Step 2. Get a list of all resources on the cluster that were deployed by Cloud Deploy. allResources, err := ce.listResources(false, namespace, resourceTypes) if err != nil { return nil, fmt.Errorf("failed to get a list of resources on the cluster, err: %w", err) } // Step 3. Get a list of resources that were deployed by Cloud Deploy as part of the latest // release on the cluster. currentResources, err := ce.listResources(true, namespace, resourceTypes) if err != nil { return nil, fmt.Errorf("failed to get a list of current resources on the cluster, err: %w", err) } // Step 4. Do a diff to determine what resources were not deployed in the latest release and // should therefore be deleted. return diffSlices(allResources, currentResources), nil } // apiResourceQueryArgs returns the args to pass to kubectl to get a list of supported resource // types on the cluster. func apiResourcesQueryArgs() []string { return []string{ "api-resources", "--verbs=list", outputFlag, nameArg, } } // kubectlGetArgs returns the args to pass to kubectl to get the resource name, // given the resource type and namespace func kubectlGetArgs(includeReleaseLabel bool, resourceType string, nspace string) []string { var labels []string if includeReleaseLabel { labels = append(labels, fmt.Sprintf("%srelease-id=%s", cloudDeployPrefix, os.Getenv(releaseEnvKey))) } labels = append(labels, fmt.Sprintf("%sdelivery-pipeline-id=%s", cloudDeployPrefix, os.Getenv(pipelineEnvKey))) labels = append(labels, fmt.Sprintf("%starget-id=%s", cloudDeployPrefix, os.Getenv(targetEnvKey))) labels = append(labels, fmt.Sprintf("%slocation=%s", cloudDeployPrefix, os.Getenv(locationEnvKey))) labels = append(labels, fmt.Sprintf("%sproject-id=%s", cloudDeployPrefix, os.Getenv(projectEnvKey))) labelsFormatted := strings.Join(labels, ",") labelArg := fmt.Sprintf("-l %s", labelsFormatted) args := []string{ "get", outputFlag, nameArg, labelArg, } if nspace != "" { args = append(args, fmt.Sprintf("--namespace=%s", nspace)) } args = append(args, resourceType) return args } // resourceTypesToQuery returns a list of resource types to query based on the command line flag value. func (ce CommandExecutor) resourceTypesToQuery(resourceType string) ([]string, error) { var resourceTypes []string // If "all" was passed get a list of all supported resource types on the // cluster. if strings.ToLower(resourceType) == "all" { apiResourcesArgs := apiResourcesQueryArgs() output, err := ce.execCommand(apiResourcesArgs) if err != nil { return nil, fmt.Errorf("failed to execute kubectl api-resources command: %w", err) } outputSplit := strings.Split(output, "\n") // Delete the empty line at the end resourceTypes = slices.DeleteFunc(outputSplit, isEmpty) return resourceTypes, nil } // This will either be the user specified resource types or the default // values. resourceTypes = strings.Split(resourceType, ",") return resourceTypes, nil } // listResources returns a list of resources, filters by resource type, namespace, // and only those deployed by Cloud Deploy. If includeReleaseLabel is true, it // filters to resources that were deployed by the current release. func (ce CommandExecutor) listResources(includeReleaseLabel bool, namespaces string, resourceTypes []string) ([]string, error) { var resources []string for _, r := range resourceTypes { res, err := ce.resourcesPerType(includeReleaseLabel, namespaces, r) if err != nil { return nil, fmt.Errorf("attempting to get resource type \"%v\" resulted in err: %w", r, err) } resources = append(resources, res...) } return resources, nil } // resourcesPerType returns a list of resources per type. func (ce CommandExecutor) resourcesPerType(includeReleaseLabel bool, namespaces string, resourceType string) ([]string, error) { var resources []string // Multiple namespaces could have been specified in the command line arg, split and loop through each. nspaces := strings.Split(namespaces, ",") for _, n := range nspaces { args := kubectlGetArgs(includeReleaseLabel, resourceType, n) output, err := ce.execCommand(args) if err != nil { return nil, fmt.Errorf("attempting to get resource type \"%v\" resulted in err: %w", resourceType, err) } if output != "" { // Separate out by line break and delete the empty line at the end. outputSplit := strings.Split(output, "\n") outputSplit = slices.DeleteFunc(outputSplit, isEmpty) resources = append(resources, outputSplit...) } } return resources, nil } // deleteResources deletes the given resources. func (ce CommandExecutor) deleteResources(resources []string) error { if len(resources) == 0 { fmt.Printf("There are no resources to delete\n") return nil } fmt.Printf("Beginning to delete resources, there are %d resources to delete\n", len(resources)) for _, resource := range resources { args := []string{"delete", resource, "--ignore-not-found=true"} _, err := ce.execCommand(args) // If the code returned is MethodNotAllowed log that and continue. This // has popped up for example when trying to delete the podmetrics resource. if strings.Contains(err.Error(), "MethodNotAllowed") { fmt.Printf("Unable to delete resource: %s. Deleting that resource is not allowed.\n Continuing on to delete other resources.", resource) continue } if err != nil { return fmt.Errorf("attempting to delete resource %v resulted in err: %w", resource, err) } } return nil } func isEmpty(e string) bool { return e == "" }