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 }