github.com/SamarSidharth/kpt@v0.0.0-20231122062228-c7d747ae3ace/pkg/live/planner/cluster.go (about)

     1  // Copyright 2022 The kpt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package planner
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"reflect"
    21  
    22  	"github.com/GoogleContainerTools/kpt/pkg/live"
    23  	"github.com/GoogleContainerTools/kpt/pkg/status"
    24  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    25  	"k8s.io/apimachinery/pkg/api/meta"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/client-go/dynamic"
    29  	"k8s.io/kubectl/pkg/cmd/util"
    30  	"sigs.k8s.io/cli-utils/pkg/apply"
    31  	"sigs.k8s.io/cli-utils/pkg/apply/event"
    32  	"sigs.k8s.io/cli-utils/pkg/common"
    33  	"sigs.k8s.io/cli-utils/pkg/inventory"
    34  	"sigs.k8s.io/cli-utils/pkg/object"
    35  )
    36  
    37  type Applier interface {
    38  	Run(ctx context.Context, invInfo inventory.Info, objects object.UnstructuredSet, options apply.ApplierOptions) <-chan event.Event
    39  }
    40  
    41  type ResourceFetcher interface {
    42  	FetchResource(ctx context.Context, id object.ObjMetadata) (*unstructured.Unstructured, bool, error)
    43  }
    44  
    45  type ClusterPlanner struct {
    46  	applier         Applier
    47  	resourceFetcher ResourceFetcher
    48  }
    49  
    50  func NewClusterPlanner(f util.Factory) (*ClusterPlanner, error) {
    51  	fetcher, err := NewResourceFetcher(f)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	invClient, err := inventory.NewClient(f, live.WrapInventoryObj, live.InvToUnstructuredFunc, inventory.StatusPolicyNone, live.ResourceGroupGVK)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	statusWatcher, err := status.NewStatusWatcher(f)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  
    66  	applier, err := apply.NewApplierBuilder().
    67  		WithFactory(f).
    68  		WithInventoryClient(invClient).
    69  		WithStatusWatcher(statusWatcher).
    70  		Build()
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	return &ClusterPlanner{
    76  		applier:         applier,
    77  		resourceFetcher: fetcher,
    78  	}, nil
    79  }
    80  
    81  type ActionType string
    82  
    83  const (
    84  	Create    ActionType = "Create"
    85  	Unchanged ActionType = "Unchanged"
    86  	Delete    ActionType = "Delete"
    87  	Update    ActionType = "Update"
    88  	Skip      ActionType = "Skip"
    89  	Error     ActionType = "Error"
    90  )
    91  
    92  type Plan struct {
    93  	Actions []Action
    94  }
    95  
    96  type Action struct {
    97  	Type      ActionType
    98  	Group     string
    99  	Kind      string
   100  	Name      string
   101  	Namespace string
   102  	Original  *unstructured.Unstructured
   103  	Updated   *unstructured.Unstructured
   104  	Error     string
   105  }
   106  
   107  type Options struct {
   108  	ServerSideOptions common.ServerSideOptions
   109  }
   110  
   111  func (r *ClusterPlanner) BuildPlan(ctx context.Context, inv inventory.Info, objects []*unstructured.Unstructured, o Options) (*Plan, error) {
   112  	actions, err := r.dryRunForPlan(ctx, inv, objects, o)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	return &Plan{
   117  		Actions: actions,
   118  	}, nil
   119  }
   120  
   121  func (r *ClusterPlanner) dryRunForPlan(
   122  	ctx context.Context,
   123  	inv inventory.Info,
   124  	objects []*unstructured.Unstructured,
   125  	o Options,
   126  ) ([]Action, error) {
   127  	eventCh := r.applier.Run(ctx, inv, objects, apply.ApplierOptions{
   128  		DryRunStrategy:    common.DryRunServer,
   129  		ServerSideOptions: o.ServerSideOptions,
   130  	})
   131  
   132  	var actions []Action
   133  	var err error
   134  	for e := range eventCh {
   135  		if e.Type == event.InitType {
   136  			// This event includes all resources that will be applied, pruned or deleted, so
   137  			// we make sure we fetch all the resources from the cluster.
   138  			// TODO: See if we can update the actuation library to provide the pre-actuation
   139  			// versions of the resources as part of the regular run. This solution is not great
   140  			// as fetching all resources will take time.
   141  			a, err := r.fetchResources(ctx, e)
   142  			if err != nil {
   143  				return nil, err
   144  			}
   145  			actions = a
   146  		}
   147  		if e.Type == event.ErrorType {
   148  			// Update the err variable here, but wait for the channel to close
   149  			// before we return from the function.
   150  			// Since ErrorEvents are considered fatal, there should only be sent
   151  			// and it will be followed by the channel being closed.
   152  			err = e.ErrorEvent.Err
   153  		}
   154  		// For the Apply, Prune and Delete event types, we just capture the result
   155  		// of the dry-run operation for the specific resource.
   156  		switch e.Type {
   157  		case event.ApplyType:
   158  			id := e.ApplyEvent.Identifier
   159  			index := indexForIdentifier(id, actions)
   160  			a := actions[index]
   161  			actions[index] = handleApplyEvent(e, a)
   162  		case event.PruneType:
   163  			id := e.PruneEvent.Identifier
   164  			index := indexForIdentifier(id, actions)
   165  			a := actions[index]
   166  			actions[index] = handlePruneEvent(e, a)
   167  		// Prune and Delete are essentially the same thing, but the actuation
   168  		// library return Prune events when resources are deleted by omission
   169  		// during apply, and Delete events from the destroyer. Supporting both
   170  		// here for completeness.
   171  		case event.DeleteType:
   172  			id := e.DeleteEvent.Identifier
   173  			index := indexForIdentifier(id, actions)
   174  			a := actions[index]
   175  			actions[index] = handleDeleteEvent(e, a)
   176  		}
   177  	}
   178  	return actions, err
   179  }
   180  
   181  func handleApplyEvent(e event.Event, a Action) Action {
   182  	if e.ApplyEvent.Error != nil {
   183  		a.Type = Error
   184  		a.Error = e.ApplyEvent.Error.Error()
   185  	} else {
   186  		switch e.ApplyEvent.Status {
   187  		case event.ApplySkipped:
   188  			a.Type = Skip
   189  		case event.ApplySuccessful:
   190  			a.Updated = e.ApplyEvent.Resource
   191  			if a.Original != nil {
   192  				// TODO: Unclear if we should diff the full resources here. It doesn't work
   193  				// well with client-side apply as the managedFields property shows up as
   194  				// changes. It also means there is a race with controllers that might change
   195  				// the status of resources.
   196  				if reflect.DeepEqual(a.Original, a.Updated) {
   197  					a.Type = Unchanged
   198  				} else {
   199  					a.Type = Update
   200  				}
   201  			} else {
   202  				a.Type = Create
   203  			}
   204  		}
   205  	}
   206  	return a
   207  }
   208  
   209  func handlePruneEvent(e event.Event, a Action) Action {
   210  	if e.PruneEvent.Error != nil {
   211  		a.Type = Error
   212  		a.Error = e.PruneEvent.Error.Error()
   213  	} else {
   214  		switch e.PruneEvent.Status {
   215  		case event.PruneSuccessful:
   216  			a.Type = Delete
   217  		// Lifecycle directives can cause resources to remain in the
   218  		// live state even if they would normally be pruned.
   219  		// TODO: Handle reason for skipped resources that has recently
   220  		// been added to the actuation library.
   221  		case event.PruneSkipped:
   222  			a.Type = Skip
   223  		}
   224  	}
   225  	return a
   226  }
   227  
   228  func handleDeleteEvent(e event.Event, a Action) Action {
   229  	if e.DeleteEvent.Error != nil {
   230  		a.Type = Error
   231  		a.Error = e.DeleteEvent.Error.Error()
   232  	} else {
   233  		switch e.DeleteEvent.Status {
   234  		case event.DeleteSuccessful:
   235  			a.Type = Delete
   236  		case event.DeleteSkipped:
   237  			a.Type = Skip
   238  		}
   239  	}
   240  	return a
   241  }
   242  
   243  func (r *ClusterPlanner) fetchResources(ctx context.Context, e event.Event) ([]Action, error) {
   244  	var actions []Action
   245  	for _, ag := range e.InitEvent.ActionGroups {
   246  		// We only care about the Apply, Prune and Delete actions.
   247  		if !(ag.Action == event.ApplyAction || ag.Action == event.PruneAction || ag.Action == event.DeleteAction) {
   248  			continue
   249  		}
   250  		for _, id := range ag.Identifiers {
   251  			u, _, err := r.resourceFetcher.FetchResource(ctx, id)
   252  			// If the type doesn't exist in the cluster, then the resource itself doesn't exist.
   253  			if err != nil && !meta.IsNoMatchError(err) {
   254  				return nil, err
   255  			}
   256  			actions = append(actions, Action{
   257  				Group:     id.GroupKind.Group,
   258  				Kind:      id.GroupKind.Kind,
   259  				Name:      id.Name,
   260  				Namespace: id.Namespace,
   261  				Original:  u,
   262  			})
   263  		}
   264  	}
   265  	return actions, nil
   266  }
   267  
   268  type resourceFetcher struct {
   269  	dynamicClient dynamic.Interface
   270  	mapper        meta.RESTMapper
   271  }
   272  
   273  func NewResourceFetcher(f util.Factory) (ResourceFetcher, error) {
   274  	dc, err := f.DynamicClient()
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  
   279  	mapper, err := f.ToRESTMapper()
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	return &resourceFetcher{
   284  		dynamicClient: dc,
   285  		mapper:        mapper,
   286  	}, nil
   287  }
   288  
   289  func (rf *resourceFetcher) FetchResource(ctx context.Context, id object.ObjMetadata) (*unstructured.Unstructured, bool, error) {
   290  	mapping, err := rf.mapper.RESTMapping(id.GroupKind)
   291  	if err != nil {
   292  		return nil, false, err
   293  	}
   294  	var r dynamic.ResourceInterface
   295  	if mapping.Scope == meta.RESTScopeRoot {
   296  		r = rf.dynamicClient.Resource(mapping.Resource)
   297  	} else {
   298  		r = rf.dynamicClient.Resource(mapping.Resource).Namespace(id.Namespace)
   299  	}
   300  	u, err := r.Get(ctx, id.Name, metav1.GetOptions{})
   301  	if err != nil && !apierrors.IsNotFound(err) {
   302  		return nil, false, err
   303  	}
   304  
   305  	if apierrors.IsNotFound(err) {
   306  		return nil, false, nil
   307  	}
   308  	return u, true, nil
   309  }
   310  
   311  func indexForIdentifier(id object.ObjMetadata, actions []Action) int {
   312  	for i := range actions {
   313  		a := actions[i]
   314  		if a.Group == id.GroupKind.Group &&
   315  			a.Kind == id.GroupKind.Kind &&
   316  			a.Name == id.Name &&
   317  			a.Namespace == id.Namespace {
   318  			return i
   319  		}
   320  	}
   321  	panic(fmt.Errorf("unknown identifier %s", id.String()))
   322  }