kubectl/pkg/cmd/apply/applyset_pruner.go

196 lines
5.5 KiB
Go

/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apply
import (
"context"
"fmt"
"sync"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/cli-runtime/pkg/printers"
"k8s.io/client-go/dynamic"
"k8s.io/klog/v2"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)
type ApplySetDeleteOptions struct {
CascadingStrategy metav1.DeletionPropagation
DryRunStrategy cmdutil.DryRunStrategy
GracePeriod int
Printer printers.ResourcePrinter
IOStreams genericiooptions.IOStreams
}
// PruneObject is an apiserver object that should be deleted as part of prune.
type PruneObject struct {
Name string
Namespace string
Mapping *meta.RESTMapping
Object runtime.Object
}
// String returns a human-readable name of the object, for use in debug messages.
func (p *PruneObject) String() string {
s := p.Mapping.GroupVersionKind.GroupKind().String()
if p.Namespace != "" {
s += " " + p.Namespace + "/" + p.Name
} else {
s += " " + p.Name
}
return s
}
// FindAllObjectsToPrune returns the list of objects that will be pruned.
// Calling this instead of Prune can be useful for dry-run / diff behaviour.
func (a *ApplySet) FindAllObjectsToPrune(ctx context.Context, dynamicClient dynamic.Interface, visitedUids sets.Set[types.UID]) ([]PruneObject, error) {
type task struct {
namespace string
restMapping *meta.RESTMapping
err error
results []PruneObject
}
var tasks []*task
// We run discovery in parallel, in as many goroutines as priority and fairness will allow
// (We don't expect many requests in real-world scenarios - maybe tens, unlikely to be hundreds)
for gvk, resource := range a.AllPrunableResources() {
scope := resource.restMapping.Scope
switch scope.Name() {
case meta.RESTScopeNameNamespace:
for _, namespace := range a.AllPrunableNamespaces() {
if namespace == "" {
// Just double-check because otherwise we get cryptic error messages
return nil, fmt.Errorf("unexpectedly encountered empty namespace during prune of namespace-scoped resource %v", gvk)
}
tasks = append(tasks, &task{
namespace: namespace,
restMapping: resource.restMapping,
})
}
case meta.RESTScopeNameRoot:
tasks = append(tasks, &task{
restMapping: resource.restMapping,
})
default:
return nil, fmt.Errorf("unhandled scope %q", scope.Name())
}
}
var wg sync.WaitGroup
for i := range tasks {
task := tasks[i]
wg.Add(1)
go func() {
defer wg.Done()
results, err := a.findObjectsToPrune(ctx, dynamicClient, visitedUids, task.namespace, task.restMapping)
if err != nil {
task.err = fmt.Errorf("listing %v objects for pruning: %w", task.restMapping.GroupVersionKind.String(), err)
} else {
task.results = results
}
}()
}
// Wait for all the goroutines to finish
wg.Wait()
var allObjects []PruneObject
for _, task := range tasks {
if task.err != nil {
return nil, task.err
}
allObjects = append(allObjects, task.results...)
}
return allObjects, nil
}
func (a *ApplySet) pruneAll(ctx context.Context, dynamicClient dynamic.Interface, visitedUids sets.Set[types.UID], deleteOptions *ApplySetDeleteOptions) error {
allObjects, err := a.FindAllObjectsToPrune(ctx, dynamicClient, visitedUids)
if err != nil {
return err
}
return a.deleteObjects(ctx, dynamicClient, allObjects, deleteOptions)
}
func (a *ApplySet) findObjectsToPrune(ctx context.Context, dynamicClient dynamic.Interface, visitedUids sets.Set[types.UID], namespace string, mapping *meta.RESTMapping) ([]PruneObject, error) {
applysetLabelSelector := a.LabelSelectorForMembers()
opt := metav1.ListOptions{
LabelSelector: applysetLabelSelector,
}
klog.V(2).Infof("listing objects for pruning; namespace=%q, resource=%v", namespace, mapping.Resource)
objects, err := dynamicClient.Resource(mapping.Resource).Namespace(namespace).List(ctx, opt)
if err != nil {
return nil, err
}
var pruneObjects []PruneObject
for i := range objects.Items {
obj := &objects.Items[i]
uid := obj.GetUID()
if visitedUids.Has(uid) {
continue
}
name := obj.GetName()
pruneObjects = append(pruneObjects, PruneObject{
Name: name,
Namespace: namespace,
Mapping: mapping,
Object: obj,
})
}
return pruneObjects, nil
}
func (a *ApplySet) deleteObjects(ctx context.Context, dynamicClient dynamic.Interface, pruneObjects []PruneObject, opt *ApplySetDeleteOptions) error {
for i := range pruneObjects {
pruneObject := &pruneObjects[i]
name := pruneObject.Name
namespace := pruneObject.Namespace
mapping := pruneObject.Mapping
if opt.DryRunStrategy != cmdutil.DryRunClient {
if err := runDelete(ctx, namespace, name, mapping, dynamicClient, opt.CascadingStrategy, opt.GracePeriod, opt.DryRunStrategy == cmdutil.DryRunServer); err != nil {
return fmt.Errorf("pruning %v: %w", pruneObject.String(), err)
}
}
opt.Printer.PrintObj(pruneObject.Object, opt.IOStreams.Out)
}
return nil
}