diff --git a/pkg/cloud/rgraph/rnode/action_update.go b/pkg/cloud/rgraph/rnode/action_update.go index 5c6e379a..e76fa0f8 100644 --- a/pkg/cloud/rgraph/rnode/action_update.go +++ b/pkg/cloud/rgraph/rnode/action_update.go @@ -18,34 +18,120 @@ package rnode import ( "context" + "fmt" + "time" "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud" + "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/api" + "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta" "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/rgraph/exec" ) -// TODO +func UpdateActions[GA any, Alpha any, Beta any]( + ops GenericOps[GA, Alpha, Beta], + got, want Node, + resource api.Resource[GA, Alpha, Beta], +) ([]exec.Action, error) { + preEvents, err := updatePreconditions(got, want) + if err != nil { + return nil, err + } + postEvents := postUpdateActionEvents(got, want) + return []exec.Action{ + newGenericUpdateAction(preEvents, ops, want.ID(), resource, postEvents), + }, nil +} + +func newGenericUpdateAction[GA any, Alpha any, Beta any]( + want exec.EventList, + ops GenericOps[GA, Alpha, Beta], + id *cloud.ResourceID, + resource api.Resource[GA, Alpha, Beta], + postEvents exec.EventList, +) *genericUpdateAction[GA, Alpha, Beta] { + return &genericUpdateAction[GA, Alpha, Beta]{ + ActionBase: exec.ActionBase{Want: want}, + ops: ops, + id: id, + resource: resource, + postEvents: postEvents, + } +} + type genericUpdateAction[GA any, Alpha any, Beta any] struct { exec.ActionBase + ops GenericOps[GA, Alpha, Beta] + id *cloud.ResourceID + resource api.Resource[GA, Alpha, Beta] + postEvents exec.EventList + + start, end time.Time } func (a *genericUpdateAction[GA, Alpha, Beta]) Run( ctx context.Context, c cloud.Cloud, ) (exec.EventList, error) { - return nil, nil + a.start = time.Now() + err := a.ops.UpdateFuncs(c).Do(ctx, "", a.id, a.resource) + a.end = time.Now() + + // Emit DropReference events for removed references. + return a.postEvents, err } func (a *genericUpdateAction[GA, Alpha, Beta]) DryRun() exec.EventList { - return nil + // Emit DropReference events for removed references. + return a.postEvents } func (a *genericUpdateAction[GA, Alpha, Beta]) String() string { - return "GenericUpdateAction TODO" + return fmt.Sprintf("GenericUpdateAction(%v)", a.id) +} + +func (a *genericUpdateAction[GA, Alpha, Beta]) Metadata() *exec.ActionMetadata { + return &exec.ActionMetadata{ + Name: fmt.Sprintf("GenericUpdateAction(%s)", a.id), + Type: exec.ActionTypeUpdate, + Summary: fmt.Sprintf("Update %s", a.id), + } } -func updatePreconditions(got, want Node) exec.EventList { +func updatePreconditions(got, want Node) (exec.EventList, error) { // Update can only occur if the resource Exists TODO: is there a case where // the ambient signal for existance from Update op collides with a // reference to it? - return nil // TODO: finish me + if got.State() != NodeExists || want.State() != NodeExists { + return nil, fmt.Errorf("node for update does not exist") + } + + outRefs := want.OutRefs() + var events exec.EventList + // Condition: references must exist before update. + for _, ref := range outRefs { + events = append(events, exec.NewExistsEvent(ref.To)) + } + return events, nil +} + +func postUpdateActionEvents(got, want Node) exec.EventList { + wantOutRefs := want.OutRefs() + gotOutRefs := got.OutRefs() + + wantRefs := make(map[meta.Key]struct{}) + for _, r := range wantOutRefs { + var empty struct{} + wantRefs[*r.To.Key] = empty + } + + // Drop reference for resources that does not exists in want Node. + var events exec.EventList + for _, wantRef := range gotOutRefs { + _, ok := wantRefs[*wantRef.To.Key] + if !ok { + events = append(events, exec.NewDropRefEvent(wantRef.From, wantRef.To)) + } + } + + return events } diff --git a/pkg/cloud/rgraph/rnode/action_update_test.go b/pkg/cloud/rgraph/rnode/action_update_test.go new file mode 100644 index 00000000..a5c20f68 --- /dev/null +++ b/pkg/cloud/rgraph/rnode/action_update_test.go @@ -0,0 +1,201 @@ +/* +Copyright 2023 Google LLC + +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 + +https://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 rnode + +import ( + "testing" + + "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud" + "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta" + "github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/rgraph/exec" +) + +const project = "proj-id" + +func globalID(name string) *cloud.ResourceID { + key := meta.GlobalKey(name) + return &cloud.ResourceID{ + Resource: "node", + APIGroup: "", + ProjectID: project, + Key: key, + } +} + +func createFakeNode(toRefs []string) Node { + fn := fakeNode{} + fn.ownership = OwnershipManaged + fn.state = NodeExists + fn.id = globalID("fn") + for _, to := range toRefs { + outRef := ResourceRef{ + From: fn.id, + To: globalID(to), + } + fn.outRefs = append(fn.outRefs, outRef) + } + return &fn +} + +func createNotExistingFakeNode() Node { + fn := fakeNode{} + fn.ownership = OwnershipManaged + fn.state = NodeDoesNotExist + fn.id = globalID("fn") + return &fn +} + +func dropRefEventList(toRefs []string) exec.EventList { + var events exec.EventList + from := globalID("fn") + for _, to := range toRefs { + events = append(events, exec.NewDropRefEvent(from, globalID(to))) + } + return events +} + +func newExistsEventList(toRefs []string) exec.EventList { + var events exec.EventList + for _, to := range toRefs { + events = append(events, exec.NewExistsEvent(globalID(to))) + } + return events +} + +func TestPostUpdateActions(t *testing.T) { + for _, tc := range []struct { + desc string + oldNode Node + newNode Node + wantEvents exec.EventList + wantErr bool + }{ + { + desc: "node's without outefs", + oldNode: createFakeNode(nil), + newNode: createFakeNode(nil), + }, + { + desc: "node's with added outefs", + oldNode: createFakeNode(nil), + newNode: createFakeNode([]string{"a", "b"}), + wantEvents: newExistsEventList([]string{"a", "b"}), + }, + { + desc: "node's with deleted outefs", + oldNode: createFakeNode([]string{"a", "b"}), + newNode: createFakeNode(nil), + }, + { + desc: "node's with replaced outef", + oldNode: createFakeNode([]string{"a", "b"}), + newNode: createFakeNode([]string{"a", "c"}), + wantEvents: newExistsEventList([]string{"a", "c"}), + }, + { + desc: "nodes don't exist", + oldNode: createNotExistingFakeNode(), + newNode: createNotExistingFakeNode(), + wantErr: true, + }, + { + desc: "new node doesn't exist", + oldNode: createFakeNode([]string{"a", "b"}), + newNode: createNotExistingFakeNode(), + wantErr: true, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + + gotEvents, err := updatePreconditions(tc.oldNode, tc.newNode) + gotErr := err != nil + if gotErr != tc.wantErr { + t.Errorf("updatePreconditions(_, _) = %v, want %v", gotErr, tc.wantErr) + } + if tc.wantErr { + return + } + if len(gotEvents) != len(tc.wantEvents) { + t.Fatalf("event's length mismatch, got: %d, want: %d", len(gotEvents), len(tc.wantEvents)) + } + for i, gotEvent := range gotEvents { + wantEvent := tc.wantEvents[i] + if !gotEvent.Equal(wantEvent) { + t.Errorf("%v != %v", gotEvent.String(), wantEvent.String()) + } + } + }) + } +} + +func TestUpdatePreconditions(t *testing.T) { + for _, tc := range []struct { + desc string + oldNode Node + newNode Node + wantEvents exec.EventList + }{ + { + desc: "node's without outefs", + oldNode: createFakeNode(nil), + newNode: createFakeNode(nil), + }, + { + desc: "node's with added outefs", + oldNode: createFakeNode(nil), + newNode: createFakeNode([]string{"a", "b"}), + }, + { + desc: "node's with deleted outefs", + oldNode: createFakeNode([]string{"a", "b"}), + newNode: createFakeNode(nil), + wantEvents: dropRefEventList([]string{"a", "b"}), + }, + { + desc: "node's with deleted first outefs", + oldNode: createFakeNode([]string{"a", "b"}), + newNode: createFakeNode([]string{"b"}), + wantEvents: dropRefEventList([]string{"a"}), + }, + { + desc: "node's with deleted outefs, random order", + oldNode: createFakeNode([]string{"a", "b", "c"}), + newNode: createFakeNode([]string{"b", "a"}), + wantEvents: dropRefEventList([]string{"c"}), + }, + { + desc: "node's with replaced outefs", + oldNode: createFakeNode([]string{"a", "b", "c"}), + newNode: createFakeNode([]string{"a", "b", "e"}), + wantEvents: dropRefEventList([]string{"c"}), + }, + } { + t.Run(tc.desc, func(t *testing.T) { + + gotEvents := postUpdateActionEvents(tc.oldNode, tc.newNode) + if len(gotEvents) != len(tc.wantEvents) { + t.Fatalf("postUpdateActionEvents(got, want) = %d, want %d", len(gotEvents), len(tc.wantEvents)) + } + for i, gotEvent := range gotEvents { + wantEvent := tc.wantEvents[i] + if !gotEvent.Equal(wantEvent) { + t.Errorf("%v != %v", gotEvent.String(), wantEvent.String()) + } + } + }) + } +}