diff --git a/generator.yaml b/generator.yaml index 380c17d..02e338a 100644 --- a/generator.yaml +++ b/generator.yaml @@ -125,6 +125,12 @@ resources: code: customPreCompare(delta, a, b) sdk_read_many_post_set_output: template_path: hooks/load_balancer/sdk_read_many_post_set_output.go.tpl + sdk_create_post_set_output: + template_path: hooks/load_balancer/sdk_create_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/load_balancer/sdk_update_pre_build_request.go.tpl + sdk_read_many_post_build_request: + template_path: hooks/load_balancer/sdk_update_post_build_request.go.tpl Listener: fields: DefaultActions.TargetGroupARN: @@ -162,6 +168,10 @@ resources: hooks: sdk_read_many_post_build_request: template_path: hooks/listener/sdk_read_many_post_build_request.go.tpl + sdk_create_post_set_output: + template_path: hooks/listener/sdk_create_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/target_group/sdk_update_pre_build_request.go.tpl TargetGroup: fields: Name: @@ -234,3 +244,5 @@ resources: template_path: hooks/rule/sdk_update_pre_build_request.go.tpl sdk_read_many_post_set_output: template_path: hooks/rule/sdk_read_many_post_set_output.go.tpl + sdk_create_post_build_request: + template_path: hooks/rule/sdk_create_post_build_request.go.tpl diff --git a/pkg/resource/listener/hooks.go b/pkg/resource/listener/hooks.go index 2be33ef..69f7d35 100644 --- a/pkg/resource/listener/hooks.go +++ b/pkg/resource/listener/hooks.go @@ -1,5 +1,18 @@ package listener +import ( + "context" + "time" + + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + "github.com/aws-controllers-k8s/elbv2-controller/pkg/resource/tags" + svcapitypes "github.com/aws-controllers-k8s/elbv2-controller/apis/v1alpha1" +) + +var ( + RequeueAfterUpdateDuration = 5 * time.Second +) + // customCheckRequiredFieldsMissingMethod returns true if there are any fields // for the ReadOne Input shape that are required but not present in the // resource's Spec or Status. @@ -8,3 +21,21 @@ func (rm *resourceManager) customCheckRequiredFieldsMissingMethod( ) bool { return r.Identifiers().ARN() == nil } + +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) ([]*svcapitypes.Tag, error) { + return tags.GetResourceTags(ctx, rm.sdkapi, rm.metrics, resourceARN ) +} + +func (rm *resourceManager) updateTags( + ctx context.Context, + desired *resource, + latest *resource, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.describeTargets") + defer func() { exit(err) }() + return tags.SyncRecourseTags(ctx, rm.sdkapi, rm.metrics, string(*desired.ko.Status.ACKResourceMetadata.ARN), desired.ko.Spec.Tags, latest.ko.Spec.Tags) +} \ No newline at end of file diff --git a/pkg/resource/listener/sdk.go b/pkg/resource/listener/sdk.go index 1d64405..875f22e 100644 --- a/pkg/resource/listener/sdk.go +++ b/pkg/resource/listener/sdk.go @@ -322,6 +322,8 @@ func (rm *resourceManager) sdkFind( } rm.setStatusDefaults(ko) + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + return &resource{ko}, nil } @@ -601,8 +603,11 @@ func (rm *resourceManager) sdkCreate( if !found { return nil, ackerr.NotFound } - rm.setStatusDefaults(ko) + if ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in CREATE"), RequeueAfterUpdateDuration) + } + return &resource{ko}, nil } @@ -855,6 +860,17 @@ func (rm *resourceManager) sdkUpdate( defer func() { exit(err) }() + + if delta.DifferentAt("Spec.Tags") { + err = rm.updateTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentAt("Spec.Tags") { + return desired, nil + } + input, err := rm.newUpdateRequestPayload(ctx, desired, delta) if err != nil { return nil, err diff --git a/pkg/resource/load_balancer/hooks.go b/pkg/resource/load_balancer/hooks.go index fc1ed18..0bc5bce 100644 --- a/pkg/resource/load_balancer/hooks.go +++ b/pkg/resource/load_balancer/hooks.go @@ -15,6 +15,7 @@ package load_balancer import ( "context" + "time" svcapitypes "github.com/aws-controllers-k8s/elbv2-controller/apis/v1alpha1" ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare" @@ -22,6 +23,11 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" svcsdktypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + "github.com/aws-controllers-k8s/elbv2-controller/pkg/resource/tags" +) + +var( + RequeueAfterUpdateDuration = 5 * time.Second ) // setResourceAdditionalFields will describe the fields that are not return by the @@ -126,7 +132,11 @@ func (rm *resourceManager) customUpdateLoadBalancer( } } // Leaving room for tag updates... - // if delta.DifferentAt("Spec.Tags") {...} + if delta.DifferentAt("Spec.Tags") { + if err := rm.updateLoadBalancerTags(ctx, desired, latest); err != nil { + return nil, err + } + } return desired, nil } @@ -169,3 +179,58 @@ func (rm *resourceManager) updateLoadBalancerAttributes( } return nil } + +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) ([]*svcapitypes.Tag, error) { + return tags.GetResourceTags(ctx, rm.sdkapi, rm.metrics, resourceARN ) +} + +func (rm *resourceManager) updateTags( + ctx context.Context, + desired *resource, + latest *resource, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.describeTargets") + defer func() { exit(err) }() + return tags.SyncRecourseTags(ctx, rm.sdkapi, rm.metrics, string(*desired.ko.Status.ACKResourceMetadata.ARN), desired.ko.Spec.Tags, latest.ko.Spec.Tags) +} + +// updateLoadBalancerTags updates the tags of the load balancer. +func (rm *resourceManager) updateLoadBalancerTags( + ctx context.Context, + desired *resource, + latest *resource, +) error { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.updateLoadBalancerTags") + var err error + defer func() { exit(err) }() + + currentTags, err := rm.getTags( + ctx, + string(*desired.ko.Status.ACKResourceMetadata.ARN), + ) + if err != nil { + return err + } + + desiredTags := []*svcapitypes.Tag{} + for _, tag := range desired.ko.Spec.Tags { + desiredTags = append(desiredTags, &svcapitypes.Tag{ + Key: tag.Key, + Value: tag.Value, + }) + } + + return tags.SyncRecourseTags( + ctx, + rm.sdkapi, + rm.metrics, + string(*desired.ko.Status.ACKResourceMetadata.ARN), + currentTags, + desiredTags, + ) +} diff --git a/pkg/resource/load_balancer/sdk.go b/pkg/resource/load_balancer/sdk.go index 45afa73..d06a312 100644 --- a/pkg/resource/load_balancer/sdk.go +++ b/pkg/resource/load_balancer/sdk.go @@ -215,6 +215,10 @@ func (rm *resourceManager) sdkFind( if err := rm.setResourceAdditionalFields(ctx, ko); err != nil { return nil, err } + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + if err != nil { + return nil, err + } return &resource{ko}, nil } @@ -396,6 +400,9 @@ func (rm *resourceManager) sdkCreate( } rm.setStatusDefaults(ko) + if ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in CREATE"), RequeueAfterUpdateDuration) + } return &resource{ko}, nil } diff --git a/pkg/resource/rule/hooks.go b/pkg/resource/rule/hooks.go index e165063..c1cb22d 100644 --- a/pkg/resource/rule/hooks.go +++ b/pkg/resource/rule/hooks.go @@ -17,16 +17,20 @@ import ( "context" "errors" "strconv" + "time" ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" svcsdktypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + svcapitypes "github.com/aws-controllers-k8s/elbv2-controller/apis/v1alpha1" + "github.com/aws-controllers-k8s/elbv2-controller/pkg/resource/tags" ) var ( // ErrInvalidPriority is an error that is returned when the priority value is invalid. ErrInvalidPriority = errors.New("invalid priority value") + RequeueAfterUpdateDuration = 5 * time.Second ) // setRulePriority sets the priority of the rule. @@ -81,3 +85,21 @@ func int32OrNil(val *int64) *int32 { } return nil } + +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) ([]*svcapitypes.Tag, error) { + return tags.GetResourceTags(ctx, rm.sdkapi, rm.metrics, resourceARN ) +} + +func (rm *resourceManager) updateTags( + ctx context.Context, + desired *resource, + latest *resource, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.describeTargets") + defer func() { exit(err) }() + return tags.SyncRecourseTags(ctx, rm.sdkapi, rm.metrics, string(*desired.ko.Status.ACKResourceMetadata.ARN), desired.ko.Spec.Tags, latest.ko.Spec.Tags) +} diff --git a/pkg/resource/rule/sdk.go b/pkg/resource/rule/sdk.go index 8b5dc28..7a0b83e 100644 --- a/pkg/resource/rule/sdk.go +++ b/pkg/resource/rule/sdk.go @@ -343,6 +343,7 @@ func (rm *resourceManager) sdkFind( rm.setStatusDefaults(ko) ko.Spec.Priority = priorityFromSDK(resp.Rules[0].Priority) + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) return &resource{ko}, nil } @@ -645,6 +646,10 @@ func (rm *resourceManager) sdkCreate( } rm.setStatusDefaults(ko) + if ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in CREATE"), RequeueAfterUpdateDuration) + } + return &resource{ko}, nil } @@ -931,6 +936,17 @@ func (rm *resourceManager) sdkUpdate( defer func() { exit(err) }() + + if delta.DifferentAt("Spec.Tags") { + err = rm.updateTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentAt("Spec.Tags") { + return desired, nil + } + if delta.DifferentAt("Spec.Priority") { if err = rm.setRulePriority(ctx, desired); err != nil { return nil, err diff --git a/pkg/resource/tags/sync.go b/pkg/resource/tags/sync.go new file mode 100644 index 0000000..2e8d0d4 --- /dev/null +++ b/pkg/resource/tags/sync.go @@ -0,0 +1,166 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 tags + +import ( + "context" + + acktags "github.com/aws-controllers-k8s/runtime/pkg/tags" + + svcapitypes "github.com/aws-controllers-k8s/elbv2-controller/apis/v1alpha1" + ackrtlog "github.com/aws-controllers-k8s/runtime/pkg/runtime/log" + ackutil "github.com/aws-controllers-k8s/runtime/pkg/util" + svcsdk "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" + svcsdktypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" +) + +var ( + _ = svcapitypes.Tag{} + _ = acktags.NewTags() + ACKSystemTags = []string{"services.k8s.aws/namespace", "services.k8s.aws/controller-version"} +) + +type metricsRecorder interface { + RecordAPICall(opType string, opID string, err error) +} + +type tagsClient interface { + DescribeTags(ctx context.Context, params *svcsdk.DescribeTagsInput, optFuncs ...func(*svcsdk.Options)) (*svcsdk.DescribeTagsOutput, error) + AddTags(ctx context.Context, params *svcsdk.AddTagsInput, optFuncs ...func(*svcsdk.Options)) (*svcsdk.AddTagsOutput, error) + RemoveTags(ctx context.Context, params *svcsdk.RemoveTagsInput, optFuncs ...func(*svcsdk.Options)) (*svcsdk.RemoveTagsOutput, error) +} + +// GetRecourceTags uses DescribeTags API Call to get the tags of a resource. +func GetResourceTags( + ctx context.Context, + client tagsClient, + mr metricsRecorder, + resourceARN string, +) ([]*svcapitypes.Tag, error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("GetRecourceTags") + defer func() { exit(nil) }() + + describeTagsResponse, err := client.DescribeTags( + ctx, + &svcsdk.DescribeTagsInput{ + ResourceArns: []string{resourceARN}, + }, + ) + mr.RecordAPICall("GET", "DescribeTags", err) + if err != nil { + return nil, err + } + + tags := make([]*svcapitypes.Tag, 0, len(describeTagsResponse.TagDescriptions)) + for _, tagDescription := range describeTagsResponse.TagDescriptions { + for _, tagStruct := range tagDescription.Tags { + tags = append(tags, &svcapitypes.Tag{ + Key: tagStruct.Key, + Value: tagStruct.Value, + }) + } + } + return tags, nil +} + +// SyncResourceTags uses TagResource and UntagResource API Calls to add, remove +// and update resource tags. +func SyncRecourseTags( + ctx context.Context, + client tagsClient, + mr metricsRecorder, + resourceARN string, + currentTags []*svcapitypes.Tag, + desiredTags []*svcapitypes.Tag, +) error { + var err error + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("SyncRecourseTags") + defer func() { exit(err) }() + + addedOrUpdated, removed := computeTagsDelta(currentTags, desiredTags) + + if len(removed) > 0 { + _, err = client.RemoveTags(ctx, &svcsdk.RemoveTagsInput{ + ResourceArns: []string{resourceARN}, + TagKeys: removed, + }) + mr.RecordAPICall("UPDATE", "RemoveTags", err) + if err != nil { + return err + } + } + + if len(addedOrUpdated) > 0 { + _, err = client.AddTags(ctx, &svcsdk.AddTagsInput{ + ResourceArns: []string{resourceARN}, + Tags: sdkTagsFromResourceTags(addedOrUpdated), + }) + mr.RecordAPICall("UPDATE", "AddTags", err) + if err != nil { + return err + } + } + return nil +} + +// computeTagsDelta compares two Tag arrays and return two different list +// containing the addedOrupdated and removed tags. The removed tags array +// only contains the tags Keys. +func computeTagsDelta( + a []*svcapitypes.Tag, + b []*svcapitypes.Tag, +) (addedOrUpdated []*svcapitypes.Tag, removed []string) { + var visitedIndexes []string +mainLoop: + for _, aElement := range a { + visitedIndexes = append(visitedIndexes, *aElement.Key) + for _, bElement := range b { + if equalStrings(aElement.Key, bElement.Key) { + if !equalStrings(aElement.Value, bElement.Value) { + addedOrUpdated = append(addedOrUpdated, bElement) + } + continue mainLoop + } + } + removed = append(removed, *aElement.Key) + } + for _, bElement := range b { + if !ackutil.InStrings(*bElement.Key, visitedIndexes) { + addedOrUpdated = append(addedOrUpdated, bElement) + } + } + return addedOrUpdated, removed +} + +// equal strings +func equalStrings(a, b *string) bool { + if a == nil { + return b == nil || *b == "" + } + return (*a == "" && b == nil) || *a == *b +} + +// svcTagsFromResourceTags transforms a *svcapitypes.Tag array to a *svcsdk.Tag array. +func sdkTagsFromResourceTags(rTags []*svcapitypes.Tag) []svcsdktypes.Tag { + tags := make([]svcsdktypes.Tag, len(rTags)) + for i := range rTags { + tags[i] = svcsdktypes.Tag{ + Key: rTags[i].Key, + Value: rTags[i].Value, + } + } + return tags +} diff --git a/pkg/resource/target_group/hooks.go b/pkg/resource/target_group/hooks.go index 4e7c528..1985f2f 100644 --- a/pkg/resource/target_group/hooks.go +++ b/pkg/resource/target_group/hooks.go @@ -25,6 +25,8 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" svcsdk "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" svcsdktypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + + "github.com/aws-controllers-k8s/elbv2-controller/pkg/resource/tags" ) var ( @@ -219,3 +221,21 @@ func extractTargetDescription(targetHealth []*svcsdktypes.TargetHealthDescriptio } return convertedTarget } + +func (rm *resourceManager) getTags( + ctx context.Context, + resourceARN string, +) ([]*svcapitypes.Tag, error) { + return tags.GetResourceTags(ctx, rm.sdkapi, rm.metrics, resourceARN ) +} + +func (rm *resourceManager) updateTags( + ctx context.Context, + desired *resource, + latest *resource, +) (err error) { + rlog := ackrtlog.FromContext(ctx) + exit := rlog.Trace("rm.describeTargets") + defer func() { exit(err) }() + return tags.SyncRecourseTags(ctx, rm.sdkapi, rm.metrics, string(*desired.ko.Status.ACKResourceMetadata.ARN), desired.ko.Spec.Tags, latest.ko.Spec.Tags) +} \ No newline at end of file diff --git a/pkg/resource/target_group/sdk.go b/pkg/resource/target_group/sdk.go index dfaca3f..3a291b6 100644 --- a/pkg/resource/target_group/sdk.go +++ b/pkg/resource/target_group/sdk.go @@ -210,6 +210,10 @@ func (rm *resourceManager) sdkFind( } rm.setStatusDefaults(ko) + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) + if err != nil { + return nil, err + } return &resource{ko}, nil } @@ -386,6 +390,10 @@ func (rm *resourceManager) sdkCreate( return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to register targets in UPDATE"), RequeueAfterUpdateDuration) } + rm.setStatusDefaults(ko) + if ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in CREATE"), RequeueAfterUpdateDuration) + } return &resource{ko}, nil } @@ -508,6 +516,17 @@ func (rm *resourceManager) sdkUpdate( defer func() { exit(err) }() + + if delta.DifferentAt("Spec.Tags") { + err = rm.updateTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentAt("Spec.Tags") { + return desired, nil + } + if delta.DifferentAt("Spec.Targets") { added, removed := getTargetsDifference(latest.ko.Spec.Targets, desired.ko.Spec.Targets) arn := (string)(*latest.ko.Status.ACKResourceMetadata.ARN) diff --git a/templates/hooks/listener/sdk_create_post_set_output.go.tpl b/templates/hooks/listener/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..77ee23c --- /dev/null +++ b/templates/hooks/listener/sdk_create_post_set_output.go.tpl @@ -0,0 +1,3 @@ + if desired.ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in UPDATE"), RequeueAfterUpdateDuration) + } \ No newline at end of file diff --git a/templates/hooks/listener/sdk_read_many_post_set_output.go.tpl b/templates/hooks/listener/sdk_read_many_post_set_output.go.tpl new file mode 100644 index 0000000..a604c7e --- /dev/null +++ b/templates/hooks/listener/sdk_read_many_post_set_output.go.tpl @@ -0,0 +1 @@ + Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) \ No newline at end of file diff --git a/templates/hooks/listener/sdk_update_pre_build_request.go.tpl b/templates/hooks/listener/sdk_update_pre_build_request.go.tpl new file mode 100644 index 0000000..1a866b7 --- /dev/null +++ b/templates/hooks/listener/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,9 @@ + if delta.DifferentAt("Spec.Tags") { + err = rm.updateTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentAt("Spec.Tags") { + return desired, nil + } \ No newline at end of file diff --git a/templates/hooks/load_balancer/sdk_create_post_set_output.go.tpl b/templates/hooks/load_balancer/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..20f2098 --- /dev/null +++ b/templates/hooks/load_balancer/sdk_create_post_set_output.go.tpl @@ -0,0 +1,3 @@ + if desired.ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in UPDATE"), RequeueAfterUpdateDuration) + } \ No newline at end of file diff --git a/templates/hooks/load_balancer/sdk_read_many_post_set_output.go.tpl b/templates/hooks/load_balancer/sdk_read_many_post_set_output.go.tpl index b1832de..2cb2031 100644 --- a/templates/hooks/load_balancer/sdk_read_many_post_set_output.go.tpl +++ b/templates/hooks/load_balancer/sdk_read_many_post_set_output.go.tpl @@ -1,3 +1,5 @@ if err := rm.setResourceAdditionalFields(ctx, ko); err != nil { return nil, err } + + Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) \ No newline at end of file diff --git a/templates/hooks/load_balancer/sdk_update_pre_build_request.go.tpl b/templates/hooks/load_balancer/sdk_update_pre_build_request.go.tpl new file mode 100644 index 0000000..1a866b7 --- /dev/null +++ b/templates/hooks/load_balancer/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,9 @@ + if delta.DifferentAt("Spec.Tags") { + err = rm.updateTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentAt("Spec.Tags") { + return desired, nil + } \ No newline at end of file diff --git a/templates/hooks/rule/sdk_create_post_set_output.go.tpl b/templates/hooks/rule/sdk_create_post_set_output.go.tpl new file mode 100644 index 0000000..20f2098 --- /dev/null +++ b/templates/hooks/rule/sdk_create_post_set_output.go.tpl @@ -0,0 +1,3 @@ + if desired.ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in UPDATE"), RequeueAfterUpdateDuration) + } \ No newline at end of file diff --git a/templates/hooks/rule/sdk_read_many_post_set_output.go.tpl b/templates/hooks/rule/sdk_read_many_post_set_output.go.tpl index 11e0728..c230033 100644 --- a/templates/hooks/rule/sdk_read_many_post_set_output.go.tpl +++ b/templates/hooks/rule/sdk_read_many_post_set_output.go.tpl @@ -1 +1,2 @@ ko.Spec.Priority = priorityFromSDK(resp.Rules[0].Priority) + ko.Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) diff --git a/templates/hooks/rule/sdk_update_pre_build_request.go.tpl b/templates/hooks/rule/sdk_update_pre_build_request.go.tpl index b6283d9..b071339 100644 --- a/templates/hooks/rule/sdk_update_pre_build_request.go.tpl +++ b/templates/hooks/rule/sdk_update_pre_build_request.go.tpl @@ -5,3 +5,13 @@ } else if !delta.DifferentExcept("Spec.Priority") { return desired, nil } + + if delta.DifferentAt("Spec.Tags") { + err = rm.updateTags(ctx, desired, latest) + if err != nil { + return nil, err + } + } + if !delta.DifferentAt("Spec.Tags") { + return desired, nil + } \ No newline at end of file diff --git a/templates/hooks/target_group/sdk_create_post_set_output.go.tpl b/templates/hooks/target_group/sdk_create_post_set_output.go.tpl index bf3e34e..19ca62c 100644 --- a/templates/hooks/target_group/sdk_create_post_set_output.go.tpl +++ b/templates/hooks/target_group/sdk_create_post_set_output.go.tpl @@ -1,3 +1,7 @@ if ko.Spec.Targets != nil { return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to register targets in UPDATE"), RequeueAfterUpdateDuration) } + + if desired.ko.Spec.Tags != nil { + return nil, ackrequeue.NeededAfter(fmt.Errorf("Requing due to tags in UPDATE"), RequeueAfterUpdateDuration) + } \ No newline at end of file diff --git a/templates/hooks/target_group/sdk_read_many_post_set_output.go.tpl b/templates/hooks/target_group/sdk_read_many_post_set_output.go.tpl index 258541e..dea4dea 100644 --- a/templates/hooks/target_group/sdk_read_many_post_set_output.go.tpl +++ b/templates/hooks/target_group/sdk_read_many_post_set_output.go.tpl @@ -4,3 +4,5 @@ } rm.setStatusDefaults(ko) + + Spec.Tags, err = rm.getTags(ctx, string(*ko.Status.ACKResourceMetadata.ARN)) \ No newline at end of file diff --git a/templates/hooks/target_group/sdk_update_pre_build_request.go.tpl b/templates/hooks/target_group/sdk_update_pre_build_request.go.tpl index ebaa35d..dcafbb6 100644 --- a/templates/hooks/target_group/sdk_update_pre_build_request.go.tpl +++ b/templates/hooks/target_group/sdk_update_pre_build_request.go.tpl @@ -17,4 +17,12 @@ if !delta.DifferentExcept("Spec.Targets") { return desired, nil + } + + if delta.DifferentAt("Spec.Priority") { + if err = rm.setRulePriority(ctx, desired); err != nil { + return nil, err + } + } else if !delta.DifferentExcept("Spec.Priority") { + return desired, nil } \ No newline at end of file diff --git a/test/e2e/resources/listener.yaml b/test/e2e/resources/listener.yaml index b1e1024..255e522 100644 --- a/test/e2e/resources/listener.yaml +++ b/test/e2e/resources/listener.yaml @@ -14,4 +14,7 @@ spec: statusCode: "HTTP_301" loadBalancerARN: $LOAD_BALANCER_ARN port: 80 - protocol: "HTTP" \ No newline at end of file + protocol: "HTTP" + tags: + - key: tagKey + value: tagValue \ No newline at end of file diff --git a/test/e2e/resources/load_balancer_tags.yaml b/test/e2e/resources/load_balancer_tags.yaml new file mode 100644 index 0000000..65912b0 --- /dev/null +++ b/test/e2e/resources/load_balancer_tags.yaml @@ -0,0 +1,12 @@ +apiVersion: elbv2.services.k8s.aws/v1alpha1 +kind: LoadBalancer +metadata: + name: $LOAD_BALANCER_NAME +spec: + name: $LOAD_BALANCER_NAME + subnets: + - $PUBLIC_SUBNET_1 + - $PUBLIC_SUBNET_2 + tags: + - key: tagKey + value: tagValue \ No newline at end of file diff --git a/test/e2e/resources/rule.yaml b/test/e2e/resources/rule.yaml index 833ba9c..c39a2be 100644 --- a/test/e2e/resources/rule.yaml +++ b/test/e2e/resources/rule.yaml @@ -13,4 +13,7 @@ spec: httpRequestMethodConfig: values: - GET - - POST \ No newline at end of file + - POST + tags: + - key: "tagKey" + value: "tagValue" \ No newline at end of file diff --git a/test/e2e/resources/target_group.yaml b/test/e2e/resources/target_group.yaml index 9dbcba0..a7c85c3 100644 --- a/test/e2e/resources/target_group.yaml +++ b/test/e2e/resources/target_group.yaml @@ -7,3 +7,6 @@ spec: targetType: lambda targets: - id: $FUNCTION_ARN_1 + tags: + - key: "tagKey" + value: "tagValue" \ No newline at end of file diff --git a/test/e2e/tests/helper.py b/test/e2e/tests/helper.py index 5726720..8fd46b1 100644 --- a/test/e2e/tests/helper.py +++ b/test/e2e/tests/helper.py @@ -68,4 +68,13 @@ def get_rule(self, arn): def rule_exists(self, arn): return self.get_rule(arn) is not None - + def get_tags(self, lb_arn): + try: + resp = self.elbv2_client.describe_tags( + ResourceArns=[lb_arn] + ) + if len(resp['TagDescriptions']) > 0: + return resp['TagDescriptions'][0]['Tags'] + return [] + except Exception as e: + return None \ No newline at end of file diff --git a/test/e2e/tests/test_listener.py b/test/e2e/tests/test_listener.py index 5672220..614fa0e 100644 --- a/test/e2e/tests/test_listener.py +++ b/test/e2e/tests/test_listener.py @@ -20,6 +20,7 @@ import pytest from acktest.k8s import resource as k8s from acktest.resources import random_suffix_name +from acktest import tags from e2e import CRD_GROUP, CRD_VERSION, load_elbv2_resource, service_marker from e2e.bootstrap_resources import get_bootstrap_resources from e2e.replacement_values import REPLACEMENT_VALUES @@ -31,7 +32,8 @@ CREATE_WAIT_AFTER_SECONDS = 10 UPDATE_WAIT_AFTER_SECONDS = 10 -DELETE_WAIT_AFTER_SECONDS = 10 +DELETE_WAIT_AFTER_SECONDS = 120 +CHECK_STATUS_WAIT_SECONDS = 300 @pytest.fixture(scope="module") def simple_listener(elbv2_client, simple_load_balancer): @@ -86,10 +88,43 @@ def test_create_delete(self, elbv2_client, simple_listener): validator = ELBValidator(elbv2_client) assert validator.listener_exists(listener_arn) + assert k8s.wait_on_condition( + ref, + "ACK.ResourceSynced", + "True", + wait_periods=CHECK_STATUS_WAIT_SECONDS // 10, + ) + + assert 'status' in cr + assert 'ackResourceMetadata' in cr['status'] + assert 'arn' in cr['status']['ackResourceMetadata'] + arn = cr['status']['ackResourceMetadata']['arn'] + + assert 'tags' in cr['spec'] + user_tags = cr['spec']['tags'] + + response_tags = validator.get_tags(arn) + + tags.assert_ack_system_tags( + tags=response_tags, + ) + + user_tags = [{"Key": d["key"], "Value": d["value"]} for d in user_tags] + tags.assert_equal_without_ack_tags( + expected=user_tags, + actual=response_tags, + ) + # Update settings updates = { "spec": { "port": 9000, + "tags": [ + { + "key": "first", + "value": "tag1" + } + ] }, } k8s.patch_custom_resource(ref, updates) @@ -98,3 +133,25 @@ def test_create_delete(self, elbv2_client, simple_listener): listener = validator.get_listener(listener_arn) assert listener is not None assert listener["Port"] == 9000 + + cr = k8s.get_resource(ref) + + assert 'status' in cr + assert 'ackResourceMetadata' in cr['status'] + assert 'arn' in cr['status']['ackResourceMetadata'] + arn = cr['status']['ackResourceMetadata']['arn'] + + assert 'tags' in cr['spec'] + user_tags = cr['spec']['tags'] + + response_tags = validator.get_tags(arn) + + tags.assert_ack_system_tags( + tags=response_tags, + ) + + user_tags = [{"Key": d["key"], "Value": d["value"]} for d in user_tags] + tags.assert_equal_without_ack_tags( + expected=user_tags, + actual=response_tags, + ) \ No newline at end of file diff --git a/test/e2e/tests/test_load_balancer.py b/test/e2e/tests/test_load_balancer.py index ef3379d..d951ca3 100644 --- a/test/e2e/tests/test_load_balancer.py +++ b/test/e2e/tests/test_load_balancer.py @@ -20,6 +20,7 @@ import pytest from acktest.k8s import resource as k8s from acktest.resources import random_suffix_name +from acktest import tags from e2e import CRD_GROUP, CRD_VERSION, load_elbv2_resource, service_marker from e2e.bootstrap_resources import get_bootstrap_resources from e2e.replacement_values import REPLACEMENT_VALUES @@ -71,6 +72,46 @@ def simple_load_balancer(elbv2_client): validator = ELBValidator(elbv2_client) assert not validator.load_balancer_exists(resource_name) +@pytest.fixture(scope="module") +def load_balancer_tags(elbv2_client): + + resource_name = random_suffix_name("lb", 16) + + replacements = REPLACEMENT_VALUES.copy() + replacements["LOAD_BALANCER_NAME"] = resource_name + + resource_data = load_elbv2_resource( + "load_balancer_tags", + additional_replacements=replacements, + ) + logging.debug(resource_data) + + # Create k8s resource + ref = k8s.CustomResourceReference( + CRD_GROUP, CRD_VERSION, RESOURCE_PLURAL, + resource_name, namespace="default", + ) + k8s.create_custom_resource(ref, resource_data) + + time.sleep(CREATE_WAIT_AFTER_SECONDS) + cr = k8s.wait_resource_consumed_by_controller(ref) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + yield (ref, cr, resource_name) + + _, deleted = k8s.delete_custom_resource( + ref, + period_length=DELETE_WAIT_AFTER_SECONDS, + ) + assert deleted + + time.sleep(DELETE_WAIT_AFTER_SECONDS) + + validator = ELBValidator(elbv2_client) + assert not validator.load_balancer_exists(resource_name) + @service_marker @pytest.mark.canary class TestLoadBalancer: @@ -104,4 +145,89 @@ def test_create_delete(self, elbv2_client, simple_load_balancer): assert attribute["Value"] == "4200" break else: - assert False, "Attribute not found" \ No newline at end of file + assert False, "Attribute not found" + + # tests create, update and delete tags + def test_tag_update(self, elbv2_client, load_balancer_tags): + (ref, cr, lb_name) = simple_load_balancer + assert lb_name is not None + + validator = ELBValidator(elbv2_client) + assert validator.load_balancer_exists(lb_name) + + # initial tags + initial = { + "spec": { + "tags": [ + { + "key": "first", + "value": "tag1" + }, + { + "key": "second", + "value": "tag2" + }, + ] + } + } + k8s.patch_custom_resource(ref, initial) + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + # Get tags from AWS + lbtags = validator.get_tags(cr["status"]["ackResourceMetadata"]["arn"]) + assert lbtags is not None + + # converting lbtags, a list of dictionaries, to a dictionary + actual_tags = {} + for tag in lbtags: + tag_key = tag["Key"] + tag_value = tag["Value"] + + actual_tags[tag_key] = tag_value + + expected_tags = { + "first": "tag1", + "second": "tag2", + } + assert actual_tags == expected_tags + # update tags + updated = { + "spec": { + "tags": [ + { + "key": "first", + "value": "updated_value" + }, + { + "key": "second", + "value": "tag2" + } + ,{ + "key": "third", + "value": "tag3" + } + ] + } + } + + k8s.patch_custom_resource(ref, updated) + time.sleep(UPDATE_WAIT_AFTER_SECONDS) + + # Get tags from AWS + lbtags = validator.get_load_balancer_tags(cr["status"]["ackResourceMetadata"]["arn"]) + assert lbtags is not None + + # converting lbtags, a list of dictionaries, to a dictionary + actual_tags = {} + for tag in lbtags: + tag_key = tag["Key"] + tag_value = tag["Value"] + + actual_tags[tag_key] = tag_value + + expected_tags = { + "first": "updated_value", + "second": "tag2", + "third": "tag3" + } + assert actual_tags == expected_tags \ No newline at end of file diff --git a/test/e2e/tests/test_rule.py b/test/e2e/tests/test_rule.py index 34992f6..8692229 100644 --- a/test/e2e/tests/test_rule.py +++ b/test/e2e/tests/test_rule.py @@ -20,6 +20,7 @@ import pytest from acktest.k8s import resource as k8s from acktest.resources import random_suffix_name +from acktest import tags from e2e import CRD_GROUP, CRD_VERSION, load_elbv2_resource, service_marker from e2e.bootstrap_resources import get_bootstrap_resources from e2e.replacement_values import REPLACEMENT_VALUES @@ -33,7 +34,8 @@ CREATE_WAIT_AFTER_SECONDS = 10 UPDATE_WAIT_AFTER_SECONDS = 10 -DELETE_WAIT_AFTER_SECONDS = 10 +DELETE_WAIT_AFTER_SECONDS = 120 +CHECK_WAIT_SECONDS = 300 @pytest.fixture(scope="module") def simple_rule(elbv2_client, simple_listener, simple_target_group, simple_load_balancer): @@ -91,6 +93,33 @@ def test_create_delete(self, elbv2_client, simple_rule): rule = validator.get_rule(rule_arn) assert rule is not None + assert k8s.wait_on_condition( + ref, + "ACK.ResourceSynced", + "True", + wait_periods=CHECK_STATUS_WAIT_SECONDS // 10, + ) + + assert 'status' in cr + assert 'ackResourceMetadata' in cr['status'] + assert 'arn' in cr['status']['ackResourceMetadata'] + arn = cr['status']['ackResourceMetadata']['arn'] + + assert 'tags' in cr['spec'] + user_tags = cr['spec']['tags'] + + response_tags = distribution.get_tags(arn) + + tags.assert_ack_system_tags( + tags=response_tags, + ) + + user_tags = [{"Key": d["key"], "Value": d["value"]} for d in user_tags] + tags.assert_equal_without_ack_tags( + expected=user_tags, + actual=response_tags, + ) + # Update settings updates = { "spec": { @@ -100,7 +129,13 @@ def test_create_delete(self, elbv2_client, simple_rule): "httpRequestMethodConfig": { "values": ["GET"] } - }] + }], + "tags": [ + { + "key": "first", + "value": "tag1" + } + ] }, } @@ -112,3 +147,25 @@ def test_create_delete(self, elbv2_client, simple_rule): assert rule["Priority"] == "500" assert rule["Conditions"][0]["Field"] == "http-request-method" assert rule["Conditions"][0]["HttpRequestMethodConfig"]["Values"] == ["GET"] + + cr = k8s.get_resource(ref) + + assert 'status' in cr + assert 'ackResourceMetadata' in cr['status'] + assert 'arn' in cr['status']['ackResourceMetadata'] + arn = cr['status']['ackResourceMetadata']['arn'] + + assert 'tags' in cr['spec'] + user_tags = cr['spec']['tags'] + + response_tags = validator.get_tags(arn) + + tags.assert_ack_system_tags( + tags=response_tags, + ) + + user_tags = [{"Key": d["key"], "Value": d["value"]} for d in user_tags] + tags.assert_equal_without_ack_tags( + expected=user_tags, + actual=response_tags, + ) \ No newline at end of file diff --git a/test/e2e/tests/test_target_groups.py b/test/e2e/tests/test_target_groups.py index d0f08f8..d5ab8d1 100644 --- a/test/e2e/tests/test_target_groups.py +++ b/test/e2e/tests/test_target_groups.py @@ -20,6 +20,7 @@ import pytest from acktest.k8s import resource as k8s from acktest.resources import random_suffix_name +from acktest import tags from e2e import CRD_GROUP, CRD_VERSION, load_elbv2_resource, service_marker from e2e.bootstrap_resources import get_bootstrap_resources from e2e.replacement_values import REPLACEMENT_VALUES @@ -29,7 +30,8 @@ CREATE_WAIT_AFTER_SECONDS = 30 UPDATE_WAIT_AFTER_SECONDS = 20 -DELETE_WAIT_AFTER_SECONDS = 10 +DELETE_WAIT_AFTER_SECONDS = 120 +CHECK_STATUS_WAIT_SECONDS = 300 @pytest.fixture(scope="module") def simple_target_group(elbv2_client): @@ -91,6 +93,34 @@ def test_create_delete(self, elbv2_client, simple_target_group): targets = validator.get_registered_targets(resource_arn) assert len(targets) == 1 assert targets[0]["Target"]["Id"] == REPLACEMENT_VALUES["FUNCTION_ARN_1"] + + assert k8s.wait_on_condition( + ref, + "ACK.ResourceSynced", + "True", + wait_periods=CHECK_STATUS_WAIT_SECONDS // 10, + ) + + assert 'status' in cr + assert 'ackResourceMetadata' in cr['status'] + assert 'arn' in cr['status']['ackResourceMetadata'] + arn = cr['status']['ackResourceMetadata']['arn'] + + assert 'tags' in cr['spec'] + user_tags = cr['spec']['tags'] + + response_tags = validator.get_tags(arn) + + tags.assert_ack_system_tags( + tags=response_tags, + ) + + user_tags = [{"Key": d["key"], "Value": d["value"]} for d in user_tags] + tags.assert_equal_without_ack_tags( + expected=user_tags, + actual=response_tags, + ) + # Update healthyThresholdCount updates = { "spec": { @@ -99,6 +129,12 @@ def test_create_delete(self, elbv2_client, simple_target_group): { "id": REPLACEMENT_VALUES["FUNCTION_ARN_2"], } + ], + "tags": [ + { + "key": "first", + "value": "tag1" + } ] }, } @@ -121,3 +157,25 @@ def test_create_delete(self, elbv2_client, simple_target_group): targets = validator.get_registered_targets(resource_arn) assert len(targets) == 0 + + cr = k8s.get_resource(ref) + + assert 'status' in cr + assert 'ackResourceMetadata' in cr['status'] + assert 'arn' in cr['status']['ackResourceMetadata'] + arn = cr['status']['ackResourceMetadata']['arn'] + + assert 'tags' in cr['spec'] + user_tags = cr['spec']['tags'] + + response_tags = validator.get_tags(arn) + + tags.assert_ack_system_tags( + tags=response_tags, + ) + + user_tags = [{"Key": d["key"], "Value": d["value"]} for d in user_tags] + tags.assert_equal_without_ack_tags( + expected=user_tags, + actual=response_tags, + )