mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-13 10:00:56 +00:00
Adds the PruneOptions and implements the methods for this struct
This commit is contained in:
258
cmd/kubectl/kubectlcobra/prune.go
Normal file
258
cmd/kubectl/kubectlcobra/prune.go
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
// Copyright 2019 The Kubernetes Authors.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// package kubectlcobra contains cobra commands from kubectl
|
||||||
|
package kubectlcobra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/cli-runtime/pkg/printers"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
"k8s.io/client-go/dynamic"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/apply"
|
||||||
|
"k8s.io/kubectl/pkg/cmd/util"
|
||||||
|
"k8s.io/kubectl/pkg/validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PruneOptions encapsulates the necessary information to
|
||||||
|
// implement the prune functionality.
|
||||||
|
type PruneOptions struct {
|
||||||
|
client dynamic.Interface
|
||||||
|
builder *resource.Builder
|
||||||
|
mapper meta.RESTMapper
|
||||||
|
namespace string
|
||||||
|
// The currently applied objects (as Infos), including the
|
||||||
|
// current grouping object. These objects are used to
|
||||||
|
// calculate the prune set after retreiving the previous
|
||||||
|
// grouping objects.
|
||||||
|
currentGroupingObject *resource.Info
|
||||||
|
// The set of retrieved grouping objects (as Infos) selected
|
||||||
|
// by the grouping label. This set should also include the
|
||||||
|
// current grouping object. Stored here to make testing
|
||||||
|
// easier by manually setting the retrieved grouping infos.
|
||||||
|
pastGroupingObjects []*resource.Info
|
||||||
|
retrievedGroupingObjects bool
|
||||||
|
|
||||||
|
toPrinter func(string) (printers.ResourcePrinter, error)
|
||||||
|
out io.Writer
|
||||||
|
|
||||||
|
validator validation.Schema
|
||||||
|
|
||||||
|
// TODO: DeleteOptions--cascade?
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPruneOptions returns a struct (PruneOptions) encapsulating the necessary
|
||||||
|
// information to run the prune. Returns an error if an error occurs
|
||||||
|
// gathering this information.
|
||||||
|
// TODO: Add dry-run options.
|
||||||
|
func NewPruneOptions(f util.Factory, ao *apply.ApplyOptions) (*PruneOptions, error) {
|
||||||
|
|
||||||
|
po := &PruneOptions{}
|
||||||
|
var err error
|
||||||
|
// Fields copied from ApplyOptions.
|
||||||
|
po.namespace = ao.Namespace
|
||||||
|
po.toPrinter = ao.ToPrinter
|
||||||
|
po.out = ao.Out
|
||||||
|
// Client/Builder fields from the Factory.
|
||||||
|
po.client, err = f.DynamicClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
po.builder = f.NewBuilder()
|
||||||
|
po.mapper, err = f.ToRESTMapper()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
po.validator, err = f.Validator(false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Retrieve/store the grouping object for current apply.
|
||||||
|
currentObjects, err := ao.GetObjects()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
currentGroupingObject, found := findGroupingObject(currentObjects)
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("Current grouping object not found during prune.")
|
||||||
|
}
|
||||||
|
po.currentGroupingObject = currentGroupingObject
|
||||||
|
// Initialize past grouping objects as empty.
|
||||||
|
po.pastGroupingObjects = []*resource.Info{}
|
||||||
|
po.retrievedGroupingObjects = false
|
||||||
|
|
||||||
|
return po, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPreviousGroupingObjects returns the set of grouping objects
|
||||||
|
// that have the same label as the current grouping object. Removes
|
||||||
|
// the current grouping object from this set. Returns an error
|
||||||
|
// if there is a problem retrieving the grouping objects.
|
||||||
|
func (po *PruneOptions) getPreviousGroupingObjects() ([]*resource.Info, error) {
|
||||||
|
|
||||||
|
// Ensures the "pastGroupingObjects" is set.
|
||||||
|
if !po.retrievedGroupingObjects {
|
||||||
|
if err := po.retrievePreviousGroupingObjects(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the current grouping info from the previous grouping infos.
|
||||||
|
currentInventory, err := infoToInventory(po.currentGroupingObject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pastGroupInfos := []*resource.Info{}
|
||||||
|
for _, pastInfo := range po.pastGroupingObjects {
|
||||||
|
pastInventory, err := infoToInventory(pastInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !currentInventory.Equals(pastInventory) {
|
||||||
|
pastGroupInfos = append(pastGroupInfos, pastInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pastGroupInfos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// retrievePreviousGroupingObjects requests the previous grouping objects
|
||||||
|
// using the grouping label from the current grouping object. Sets
|
||||||
|
// the field "pastGroupingObjects". Returns an error if the grouping
|
||||||
|
// label doesn't exist for the current currentGroupingObject does not
|
||||||
|
// exist or if the call to retrieve the past grouping objects fails.
|
||||||
|
func (po *PruneOptions) retrievePreviousGroupingObjects() error {
|
||||||
|
// Get the grouping label for this grouping object, and create
|
||||||
|
// a label selector from it.
|
||||||
|
if po.currentGroupingObject == nil || po.currentGroupingObject.Object == nil {
|
||||||
|
return fmt.Errorf("Missing current grouping object.\n")
|
||||||
|
}
|
||||||
|
groupingLabel, err := retrieveGroupingLabel(po.currentGroupingObject.Object)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
labelSelector := fmt.Sprintf("%s=%s", GroupingLabel, groupingLabel)
|
||||||
|
retrievedGroupingInfos, err := po.builder.
|
||||||
|
Unstructured().
|
||||||
|
// TODO: Check if this validator is necessary.
|
||||||
|
Schema(po.validator).
|
||||||
|
ContinueOnError().
|
||||||
|
NamespaceParam(po.namespace).DefaultNamespace().
|
||||||
|
ResourceTypes("configmap").
|
||||||
|
LabelSelectorParam(labelSelector).
|
||||||
|
Flatten().
|
||||||
|
Do().
|
||||||
|
Infos()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
po.pastGroupingObjects = retrievedGroupingInfos
|
||||||
|
po.retrievedGroupingObjects = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// infoToInventory transforms the object represented by the passed "info"
|
||||||
|
// into its Inventory representation. Returns error if the passed Info
|
||||||
|
// is nil, or the Object in the Info is empty.
|
||||||
|
func infoToInventory(info *resource.Info) (*Inventory, error) {
|
||||||
|
if info == nil || info.Object == nil {
|
||||||
|
return nil, fmt.Errorf("Empty resource.Info can not calculate as inventory.\n")
|
||||||
|
}
|
||||||
|
obj := info.Object
|
||||||
|
gk := obj.GetObjectKind().GroupVersionKind().GroupKind()
|
||||||
|
return createInventory(info.Namespace, info.Name, gk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unionPastInventory takes a set of grouping objects (infos), returning the
|
||||||
|
// union of the objects referenced by these grouping objects as an
|
||||||
|
// InventorySet. Returns an error if any of the passed objects are not
|
||||||
|
// grouping objects, or if unable to retrieve the inventory from any
|
||||||
|
// grouping object.
|
||||||
|
func unionPastInventory(infos []*resource.Info) (*InventorySet, error) {
|
||||||
|
inventorySet := NewInventorySet([]*Inventory{})
|
||||||
|
for _, info := range infos {
|
||||||
|
inv, err := retrieveInventoryFromGroupingObj([]*resource.Info{info})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inventorySet.AddItems(inv)
|
||||||
|
}
|
||||||
|
return inventorySet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcPruneSet returns the InventorySet representing the objects to
|
||||||
|
// delete (prune). pastGroupInfos are the set of past applied grouping
|
||||||
|
// objects, storing the inventory of the objects applied at the same time.
|
||||||
|
// Calculates the prune set as:
|
||||||
|
//
|
||||||
|
// prune set = (prev1 U prev2 U ... U prevN) - (curr1, curr2, ..., currN)
|
||||||
|
//
|
||||||
|
// Returns an error if we are unable to retrieve the set of previously
|
||||||
|
// applied objects, or if we are unable to get the currently applied objects
|
||||||
|
// from the current grouping object.
|
||||||
|
func (po *PruneOptions) calcPruneSet(pastGroupingInfos []*resource.Info) (*InventorySet, error) {
|
||||||
|
pastInventory, err := unionPastInventory(pastGroupingInfos)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Current grouping object as inventory set.
|
||||||
|
c := []*resource.Info{po.currentGroupingObject}
|
||||||
|
currentInv, err := retrieveInventoryFromGroupingObj(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pastInventory.Subtract(NewInventorySet(currentInv))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune deletes the set of resources which were previously applied
|
||||||
|
// (retrieved from previous grouping objects) but omitted in
|
||||||
|
// the current apply. Prune also delete all previous grouping
|
||||||
|
// objects. Returns an error if there was a problem.
|
||||||
|
func (po *PruneOptions) Prune() error {
|
||||||
|
|
||||||
|
// Retrieve previous grouping objects, and calculate the
|
||||||
|
// union of the previous applies as an inventory set.
|
||||||
|
pastGroupingInfos, err := po.getPreviousGroupingObjects()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pruneSet, err := po.calcPruneSet(pastGroupingInfos)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the prune objects.
|
||||||
|
for _, inv := range pruneSet.GetItems() {
|
||||||
|
mapping, err := po.mapper.RESTMapping(inv.GroupKind)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = po.client.Resource(mapping.Resource).Namespace(inv.Namespace).Delete(inv.Name, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(po.out, "%s/%s deleted\n", strings.ToLower(inv.GroupKind.Kind), inv.Name)
|
||||||
|
}
|
||||||
|
// Delete previous grouping objects.
|
||||||
|
for _, pastGroupInfo := range pastGroupingInfos {
|
||||||
|
err = po.client.Resource(pastGroupInfo.Mapping.Resource).
|
||||||
|
Namespace(pastGroupInfo.Namespace).
|
||||||
|
Delete(pastGroupInfo.Name, &metav1.DeleteOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printer, err := po.toPrinter("deleted")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = printer.PrintObj(pastGroupInfo.Object, po.out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
244
cmd/kubectl/kubectlcobra/prune_test.go
Normal file
244
cmd/kubectl/kubectlcobra/prune_test.go
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
// Copyright 2019 The Kubernetes Authors.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// package kubectlcobra contains cobra commands from kubectl
|
||||||
|
package kubectlcobra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/cli-runtime/pkg/resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pod1Inv = &Inventory{
|
||||||
|
Namespace: testNamespace,
|
||||||
|
Name: pod1Name,
|
||||||
|
GroupKind: schema.GroupKind{
|
||||||
|
Group: "",
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pod2Inv = &Inventory{
|
||||||
|
Namespace: testNamespace,
|
||||||
|
Name: pod2Name,
|
||||||
|
GroupKind: schema.GroupKind{
|
||||||
|
Group: "",
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pod3Inv = &Inventory{
|
||||||
|
Namespace: testNamespace,
|
||||||
|
Name: pod3Name,
|
||||||
|
GroupKind: schema.GroupKind{
|
||||||
|
Group: "",
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupingInv = &Inventory{
|
||||||
|
Namespace: testNamespace,
|
||||||
|
Name: groupingObjName,
|
||||||
|
GroupKind: schema.GroupKind{
|
||||||
|
Group: "",
|
||||||
|
Kind: "ConfigMap",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfoToInventory(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
info *resource.Info
|
||||||
|
expected *Inventory
|
||||||
|
isError bool
|
||||||
|
}{
|
||||||
|
"Nil info is an error": {
|
||||||
|
info: nil,
|
||||||
|
expected: nil,
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
"Nil info object is an error": {
|
||||||
|
info: nilInfo,
|
||||||
|
expected: nil,
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
"Pod 1 object becomes Pod 1 inventory": {
|
||||||
|
info: pod1Info,
|
||||||
|
expected: pod1Inv,
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
"Grouping object becomes grouping inventory": {
|
||||||
|
info: copyGroupingInfo(),
|
||||||
|
expected: groupingInv,
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
actual, err := infoToInventory(test.info)
|
||||||
|
if test.isError && err == nil {
|
||||||
|
t.Errorf("Did not receive expected error.\n")
|
||||||
|
}
|
||||||
|
if !test.isError {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Receieved unexpected error: %s\n", err)
|
||||||
|
}
|
||||||
|
if !test.expected.Equals(actual) {
|
||||||
|
t.Errorf("Expected inventory (%s), got (%s)\n", test.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a grouping object with the inventory set from
|
||||||
|
// the passed "children".
|
||||||
|
func createGroupingInfo(name string, children ...(*resource.Info)) *resource.Info {
|
||||||
|
groupingObjCopy := groupingObj.DeepCopy()
|
||||||
|
var groupingInfo = &resource.Info{
|
||||||
|
Namespace: testNamespace,
|
||||||
|
Name: groupingObjName,
|
||||||
|
Object: groupingObjCopy,
|
||||||
|
}
|
||||||
|
infos := []*resource.Info{groupingInfo}
|
||||||
|
infos = append(infos, children...)
|
||||||
|
_ = addInventoryToGroupingObj(infos)
|
||||||
|
return groupingInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnionPastInventory(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
groupingInfos []*resource.Info
|
||||||
|
expected []*Inventory
|
||||||
|
}{
|
||||||
|
"Empty grouping objects = empty inventory set": {
|
||||||
|
groupingInfos: []*resource.Info{},
|
||||||
|
expected: []*Inventory{},
|
||||||
|
},
|
||||||
|
"No children in grouping object, equals no inventory": {
|
||||||
|
groupingInfos: []*resource.Info{createGroupingInfo("test-1")},
|
||||||
|
expected: []*Inventory{},
|
||||||
|
},
|
||||||
|
"Grouping object with Pod1 returns inventory with Pod1": {
|
||||||
|
groupingInfos: []*resource.Info{createGroupingInfo("test-1", pod1Info)},
|
||||||
|
expected: []*Inventory{pod1Inv},
|
||||||
|
},
|
||||||
|
"Grouping object with three pods returns inventory with three pods": {
|
||||||
|
groupingInfos: []*resource.Info{
|
||||||
|
createGroupingInfo("test-1", pod1Info, pod2Info, pod3Info),
|
||||||
|
},
|
||||||
|
expected: []*Inventory{pod1Inv, pod2Inv, pod3Inv},
|
||||||
|
},
|
||||||
|
"Two grouping objects with different pods returns inventory with both pods": {
|
||||||
|
groupingInfos: []*resource.Info{
|
||||||
|
createGroupingInfo("test-1", pod1Info),
|
||||||
|
createGroupingInfo("test-2", pod2Info),
|
||||||
|
},
|
||||||
|
expected: []*Inventory{pod1Inv, pod2Inv},
|
||||||
|
},
|
||||||
|
"Two grouping objects with overlapping pods returns set of pods": {
|
||||||
|
groupingInfos: []*resource.Info{
|
||||||
|
createGroupingInfo("test-1", pod1Info, pod2Info),
|
||||||
|
createGroupingInfo("test-2", pod2Info, pod3Info),
|
||||||
|
},
|
||||||
|
expected: []*Inventory{pod1Inv, pod2Inv, pod3Inv},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
actual, err := unionPastInventory(test.groupingInfos)
|
||||||
|
expected := NewInventorySet(test.expected)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error received: %s\n", err)
|
||||||
|
}
|
||||||
|
if !expected.Equals(actual) {
|
||||||
|
t.Errorf("Expected inventory (%s), got (%s)\n", expected, actual)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCalcPruneSet(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
past []*resource.Info
|
||||||
|
current *resource.Info
|
||||||
|
expected []*Inventory
|
||||||
|
isError bool
|
||||||
|
}{
|
||||||
|
"Object not unstructured--error": {
|
||||||
|
past: []*resource.Info{nonUnstructuredGroupingInfo},
|
||||||
|
current: &resource.Info{},
|
||||||
|
expected: []*Inventory{},
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
"No past group objects--no prune set": {
|
||||||
|
|
||||||
|
past: []*resource.Info{},
|
||||||
|
current: createGroupingInfo("test-1"),
|
||||||
|
expected: []*Inventory{},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
"Empty past grouping object--no prune set": {
|
||||||
|
past: []*resource.Info{createGroupingInfo("test-1")},
|
||||||
|
current: createGroupingInfo("test-1"),
|
||||||
|
expected: []*Inventory{},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
"Pod1 - Pod1 = empty set": {
|
||||||
|
past: []*resource.Info{
|
||||||
|
createGroupingInfo("test-1", pod1Info),
|
||||||
|
},
|
||||||
|
current: createGroupingInfo("test-1", pod1Info),
|
||||||
|
expected: []*Inventory{},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
"(Pod1, Pod2) - Pod1 = Pod2": {
|
||||||
|
past: []*resource.Info{
|
||||||
|
createGroupingInfo("test-1", pod1Info, pod2Info),
|
||||||
|
},
|
||||||
|
current: createGroupingInfo("test-1", pod1Info),
|
||||||
|
expected: []*Inventory{pod2Inv},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
"(Pod1, Pod2) - Pod2 = Pod1": {
|
||||||
|
past: []*resource.Info{
|
||||||
|
createGroupingInfo("test-1", pod1Info, pod2Info),
|
||||||
|
},
|
||||||
|
current: createGroupingInfo("test-1", pod2Info),
|
||||||
|
expected: []*Inventory{pod1Inv},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
"(Pod1, Pod2, Pod3) - Pod2 = Pod1, Pod3": {
|
||||||
|
past: []*resource.Info{
|
||||||
|
createGroupingInfo("test-1", pod1Info, pod2Info),
|
||||||
|
createGroupingInfo("test-1", pod2Info, pod3Info),
|
||||||
|
},
|
||||||
|
current: createGroupingInfo("test-1", pod2Info),
|
||||||
|
expected: []*Inventory{pod1Inv, pod3Inv},
|
||||||
|
isError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
po := &PruneOptions{}
|
||||||
|
po.currentGroupingObject = test.current
|
||||||
|
actual, err := po.calcPruneSet(test.past)
|
||||||
|
expected := NewInventorySet(test.expected)
|
||||||
|
if test.isError && err == nil {
|
||||||
|
t.Errorf("Did not receive expected error.\n")
|
||||||
|
}
|
||||||
|
if !test.isError {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error received: %s\n", err)
|
||||||
|
}
|
||||||
|
if !expected.Equals(actual) {
|
||||||
|
t.Errorf("Expected prune set (%s), got (%s)\n", expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user