From c571d3ebb1ca1ce24be56da3a28be61a39275a2f Mon Sep 17 00:00:00 2001 From: Amine Hilaly Date: Thu, 3 Jun 2021 00:26:09 +0200 Subject: [PATCH] Add `Table` resource custom hooks, terminalCodes, printcolumns and e2e tests - Add custom hooks, terminalCodes, printcomns to `generator.yaml` - Regenerate `Table` resource - Add e2e tests for create and delete operations --- apis/v1alpha1/table.go | 2 + .../dynamodb.services.k8s.aws_tables.yaml | 9 +- generator.yaml | 17 ++++ pkg/resource/table/conditions.go | 93 ++++++++++++++++++ pkg/resource/table/hooks.go | 97 +++++++++++++++++++ pkg/resource/table/sdk.go | 50 +++++++++- .../table/sdk_delete_pre_build_request.go.tpl | 6 ++ .../table/sdk_read_one_post_set_output.go.tpl | 6 ++ .../table/sdk_update_pre_build_request.go.tpl | 21 ++++ test/e2e/.gitignore | 3 + test/e2e/__init__.py | 63 ++++++++++++ test/e2e/bootstrap_resources.py | 35 +++++++ test/e2e/conftest.py | 46 +++++++++ test/e2e/replacement_values.py | 20 ++++ test/e2e/requirements.txt | 1 + test/e2e/resources/table.yaml | 33 +++++++ test/e2e/service_bootstrap.py | 33 +++++++ test/e2e/service_cleanup.py | 33 +++++++ test/e2e/tests/test_table.py | 97 +++++++++++++++++++ 19 files changed, 662 insertions(+), 3 deletions(-) create mode 100644 pkg/resource/table/conditions.go create mode 100644 pkg/resource/table/hooks.go create mode 100644 templates/hooks/table/sdk_delete_pre_build_request.go.tpl create mode 100644 templates/hooks/table/sdk_read_one_post_set_output.go.tpl create mode 100644 templates/hooks/table/sdk_update_pre_build_request.go.tpl create mode 100644 test/e2e/.gitignore create mode 100644 test/e2e/__init__.py create mode 100644 test/e2e/bootstrap_resources.py create mode 100644 test/e2e/conftest.py create mode 100644 test/e2e/replacement_values.py create mode 100644 test/e2e/requirements.txt create mode 100644 test/e2e/resources/table.yaml create mode 100644 test/e2e/service_bootstrap.py create mode 100644 test/e2e/service_cleanup.py create mode 100644 test/e2e/tests/test_table.py diff --git a/apis/v1alpha1/table.go b/apis/v1alpha1/table.go index 37d1593..dfed694 100644 --- a/apis/v1alpha1/table.go +++ b/apis/v1alpha1/table.go @@ -230,6 +230,8 @@ type TableStatus struct { // Table is the Schema for the Tables API // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.tableStatus` +// +kubebuilder:printcolumn:name="TableName",type=string,JSONPath=`.spec.tableName` type Table struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml b/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml index 5b4beef..5644a59 100644 --- a/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml +++ b/config/crd/bases/dynamodb.services.k8s.aws_tables.yaml @@ -16,7 +16,14 @@ spec: singular: table scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .status.tableStatus + name: Status + type: string + - jsonPath: .spec.tableName + name: TableName + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Table is the Schema for the Tables API diff --git a/generator.yaml b/generator.yaml index 41be9ed..717bc2f 100644 --- a/generator.yaml +++ b/generator.yaml @@ -1,9 +1,26 @@ resources: Table: + fields: + TableStatus: + is_printable: true + print_name: Status + TableName: + is_printable: true exceptions: errors: 404: code: ResourceNotFoundException + terminal_codes: + - InternalServerError + - LimitExceededException + - ResourceInUseException + hooks: + sdk_read_one_post_set_output: + template_path: hooks/table/sdk_read_one_post_set_output.go.tpl + sdk_update_pre_build_request: + template_path: hooks/table/sdk_update_pre_build_request.go.tpl + sdk_delete_pre_build_request: + template_path: hooks/table/sdk_delete_pre_build_request.go.tpl GlobalTable: exceptions: errors: diff --git a/pkg/resource/table/conditions.go b/pkg/resource/table/conditions.go new file mode 100644 index 0000000..6770fcf --- /dev/null +++ b/pkg/resource/table/conditions.go @@ -0,0 +1,93 @@ +// 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 table + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// getSyncedCondition returns the Condition in the resource's Conditions +// collection that is of type ConditionTypeResourceSynced. If no such condition +// is found, returns nil. +// +// TODO(jaypipes): Move to ACK code-gen templates. +func getSyncedCondition(r *resource) *ackv1alpha1.Condition { + return getConditionOfType(r, ackv1alpha1.ConditionTypeResourceSynced) +} + +// getConditionOfType returns the Condition in the resource's Conditions +// collection of the supplied type. If no such condition is found, returns nil. +// +// TODO(jaypipes): Move to ACK code-gen templates. +func getConditionOfType( + r *resource, + condType ackv1alpha1.ConditionType, +) *ackv1alpha1.Condition { + for _, condition := range r.ko.Status.Conditions { + if condition.Type == condType { + return condition + } + } + return nil +} + +// setSyncedCondition sets the resource's Condition of type +// ConditionTypeResourceSynced to the supplied status, optional message and +// reason. +// +// TODO(jaypipes): Move to ACK code-gen templates. +func setSyncedCondition( + r *resource, + status corev1.ConditionStatus, + message *string, + reason *string, +) { + c := getSyncedCondition(r) + if c == nil { + c = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeResourceSynced, + } + r.ko.Status.Conditions = append(r.ko.Status.Conditions, c) + } + now := metav1.Now() + c.LastTransitionTime = &now + c.Status = status +} + +// setTerminalCondition sets the resource's Condition of type +// ConditionTypeTerminal to the supplied status, optional message and reason. +// +// TODO(jaypipes): Move to ACK code-gen templates. +func setTerminalCondition( + r *resource, + status corev1.ConditionStatus, + message *string, + reason *string, +) { + c := getSyncedCondition(r) + if c == nil { + c = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeTerminal, + } + r.ko.Status.Conditions = append(r.ko.Status.Conditions, c) + } + now := metav1.Now() + c.LastTransitionTime = &now + c.Status = status + c.Message = message + c.Reason = reason +} diff --git a/pkg/resource/table/hooks.go b/pkg/resource/table/hooks.go new file mode 100644 index 0000000..bde4c29 --- /dev/null +++ b/pkg/resource/table/hooks.go @@ -0,0 +1,97 @@ +// 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 table + +import ( + "errors" + "time" + + "github.com/aws-controllers-k8s/dynamodb-controller/apis/v1alpha1" + ackrequeue "github.com/aws-controllers-k8s/runtime/pkg/requeue" +) + +var ( + ErrTableDeleting = errors.New("table in 'DELETING' state, cannot be modified or deleted") + ErrTableCreating = errors.New("table in 'CREATING' state, cannot be modified or deleted") + ErrTableUpdating = errors.New("table in 'UPDATING' state, cannot be modified or deleted") +) + +var ( + // TerminalStatuses are the status strings that are terminal states for a + // DynamoDB table + TerminalStatuses = []v1alpha1.TableStatus_SDK{ + v1alpha1.TableStatus_SDK_ARCHIVING, + v1alpha1.TableStatus_SDK_DELETING, + } +) + +var ( + requeueWaitWhileDeleting = ackrequeue.NeededAfter( + ErrTableDeleting, + 5*time.Second, + ) + requeueWaitWhileCreating = ackrequeue.NeededAfter( + ErrTableDeleting, + 5*time.Second, + ) + requeueWaitWhileUpdating = ackrequeue.NeededAfter( + ErrTableUpdating, + 5*time.Second, + ) +) + +// tableHasTerminalStatus returns whether the supplied Dynamodb table is in a +// terminal state +func tableHasTerminalStatus(r *resource) bool { + if r.ko.Status.TableStatus == nil { + return false + } + ts := *r.ko.Status.TableStatus + for _, s := range TerminalStatuses { + if ts == string(s) { + return true + } + } + return false +} + +// tableCreating returns true if the supplied DynamodbDB table is in the process +// of being created +func tableCreating(r *resource) bool { + if r.ko.Status.TableStatus == nil { + return false + } + dbis := *r.ko.Status.TableStatus + return dbis == string(v1alpha1.TableStatus_SDK_CREATING) +} + +// tableDeleting returns true if the supplied DynamodbDB table is in the process +// of being deleted +func tableDeleting(r *resource) bool { + if r.ko.Status.TableStatus == nil { + return false + } + dbis := *r.ko.Status.TableStatus + return dbis == string(v1alpha1.TableStatus_SDK_DELETING) +} + +// tableUpdating returns true if the supplied DynamodbDB table is in the process +// of being deleted +func tableUpdating(r *resource) bool { + if r.ko.Status.TableStatus == nil { + return false + } + dbis := *r.ko.Status.TableStatus + return dbis == string(v1alpha1.TableStatus_SDK_UPDATING) +} diff --git a/pkg/resource/table/sdk.go b/pkg/resource/table/sdk.go index 66d8f9a..5c39ee2 100644 --- a/pkg/resource/table/sdk.go +++ b/pkg/resource/table/sdk.go @@ -394,6 +394,12 @@ func (rm *resourceManager) sdkFind( rm.setStatusDefaults(ko) + if tableCreating(&resource{ko}) { + return &resource{ko}, requeueWaitWhileCreating + } + if tableUpdating(&resource{ko}) { + return &resource{ko}, requeueWaitWhileUpdating + } return &resource{ko}, nil } @@ -803,6 +809,27 @@ func (rm *resourceManager) sdkUpdate( latest *resource, delta *ackcompare.Delta, ) (*resource, error) { + if tableDeleting(latest) { + msg := "table is currently being deleted" + setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil) + return desired, requeueWaitWhileDeleting + } + if tableCreating(latest) { + msg := "table is currently being created" + setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil) + return desired, requeueWaitWhileCreating + } + if tableUpdating(latest) { + msg := "table is currently being created" + setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil) + return desired, requeueWaitWhileUpdating + } + if tableHasTerminalStatus(latest) { + msg := "table is in '" + *latest.ko.Status.TableStatus + "' status" + setTerminalCondition(desired, corev1.ConditionTrue, &msg, nil) + setSyncedCondition(desired, corev1.ConditionTrue, nil, nil) + return desired, nil + } input, err := rm.newUpdateRequestPayload(ctx, desired) if err != nil { @@ -1057,6 +1084,12 @@ func (rm *resourceManager) sdkDelete( ctx context.Context, r *resource, ) error { + if tableDeleting(r) { + return requeueWaitWhileDeleting + } + if tableUpdating(r) { + return requeueWaitWhileUpdating + } input, err := rm.newDeleteRequestPayload(r) if err != nil { @@ -1165,6 +1198,19 @@ func (rm *resourceManager) updateConditions( // and if the exception indicates that it is a Terminal exception // 'Terminal' exception are specified in generator configuration func (rm *resourceManager) terminalAWSError(err error) bool { - // No terminal_errors specified for this resource in generator config - return false + if err == nil { + return false + } + awsErr, ok := ackerr.AWSError(err) + if !ok { + return false + } + switch awsErr.Code() { + case "InternalServerError", + "LimitExceededException", + "ResourceInUseException": + return true + default: + return false + } } diff --git a/templates/hooks/table/sdk_delete_pre_build_request.go.tpl b/templates/hooks/table/sdk_delete_pre_build_request.go.tpl new file mode 100644 index 0000000..2982de9 --- /dev/null +++ b/templates/hooks/table/sdk_delete_pre_build_request.go.tpl @@ -0,0 +1,6 @@ + if tableDeleting(r) { + return requeueWaitWhileDeleting + } + if tableUpdating(r) { + return requeueWaitWhileUpdating + } \ No newline at end of file diff --git a/templates/hooks/table/sdk_read_one_post_set_output.go.tpl b/templates/hooks/table/sdk_read_one_post_set_output.go.tpl new file mode 100644 index 0000000..b318aeb --- /dev/null +++ b/templates/hooks/table/sdk_read_one_post_set_output.go.tpl @@ -0,0 +1,6 @@ + if tableCreating(&resource{ko}) { + return &resource{ko}, requeueWaitWhileCreating + } + if tableUpdating(&resource{ko}) { + return &resource{ko}, requeueWaitWhileUpdating + } \ No newline at end of file diff --git a/templates/hooks/table/sdk_update_pre_build_request.go.tpl b/templates/hooks/table/sdk_update_pre_build_request.go.tpl new file mode 100644 index 0000000..6b7de9d --- /dev/null +++ b/templates/hooks/table/sdk_update_pre_build_request.go.tpl @@ -0,0 +1,21 @@ + if tableDeleting(latest) { + msg := "table is currently being deleted" + setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil) + return desired, requeueWaitWhileDeleting + } + if tableCreating(latest) { + msg := "table is currently being created" + setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil) + return desired, requeueWaitWhileCreating + } + if tableUpdating(latest) { + msg := "table is currently being created" + setSyncedCondition(desired, corev1.ConditionFalse, &msg, nil) + return desired, requeueWaitWhileUpdating + } + if tableHasTerminalStatus(latest) { + msg := "table is in '"+*latest.ko.Status.TableStatus+"' status" + setTerminalCondition(desired, corev1.ConditionTrue, &msg, nil) + setSyncedCondition(desired, corev1.ConditionTrue, nil, nil) + return desired, nil + } \ No newline at end of file diff --git a/test/e2e/.gitignore b/test/e2e/.gitignore new file mode 100644 index 0000000..1700a50 --- /dev/null +++ b/test/e2e/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.py[cod] +**/bootstrap.yaml \ No newline at end of file diff --git a/test/e2e/__init__.py b/test/e2e/__init__.py new file mode 100644 index 0000000..6c75319 --- /dev/null +++ b/test/e2e/__init__.py @@ -0,0 +1,63 @@ +# 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. + +import boto3 +import pytest +import time +import logging +from typing import Dict, Any +from pathlib import Path + +from acktest.k8s import resource as k8s +from acktest.resources import load_resource_file + +SERVICE_NAME = "dynamodb" +CRD_GROUP = "dynamodb.services.k8s.aws" +CRD_VERSION = "v1alpha1" + +# PyTest marker for the current service +service_marker = pytest.mark.service(arg=SERVICE_NAME) + +bootstrap_directory = Path(__file__).parent +resource_directory = Path(__file__).parent / "resources" + +def load_dynamodb_resource(resource_name: str, additional_replacements: Dict[str, Any] = {}): + """ Overrides the default `load_resource_file` to access the specific resources + directory for the current service. + """ + return load_resource_file(resource_directory, resource_name, additional_replacements=additional_replacements) + +def wait_for_cr_status( + reference: k8s.CustomResourceReference, + status_field: str, + desired_status: str, + wait_periods: int, + period_length: int, +): + """ + Waits for the specified condition in CR status to reach the desired value. + """ + actual_status = None + for _ in range(wait_periods): + time.sleep(period_length) + resource = k8s.get_resource(reference) + actual_status = resource["status"][status_field] + if actual_status == desired_status: + break + + else: + logging.error( + f"Wait for status: {desired_status} timed out. Actual status: {actual_status}" + ) + + assert actual_status == desired_status \ No newline at end of file diff --git a/test/e2e/bootstrap_resources.py b/test/e2e/bootstrap_resources.py new file mode 100644 index 0000000..eaefcce --- /dev/null +++ b/test/e2e/bootstrap_resources.py @@ -0,0 +1,35 @@ +# 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. + +"""Declares the structure of the bootstrapped resources and provides a loader +for them. +""" + +from dataclasses import dataclass + +from e2e import bootstrap_directory +from acktest.resources import read_bootstrap_config + +@dataclass +class TestBootstrapResources: + pass + +_bootstrap_resources = None + +def get_bootstrap_resources(bootstrap_file_name: str = "bootstrap.yaml"): + global _bootstrap_resources + if _bootstrap_resources is None: + _bootstrap_resources = TestBootstrapResources( + **read_bootstrap_config(bootstrap_directory, bootstrap_file_name=bootstrap_file_name), + ) + return _bootstrap_resources \ No newline at end of file diff --git a/test/e2e/conftest.py b/test/e2e/conftest.py new file mode 100644 index 0000000..118414c --- /dev/null +++ b/test/e2e/conftest.py @@ -0,0 +1,46 @@ +# 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. + +import os +import pytest + +from acktest import k8s + + +def pytest_addoption(parser): + parser.addoption("--runslow", action="store_true", default=False, help="run slow tests") + + +def pytest_configure(config): + config.addinivalue_line( + "markers", "canary: mark test to also run in canary tests" + ) + config.addinivalue_line( + "markers", "service(arg): mark test associated with a given service" + ) + config.addinivalue_line( + "markers", "slow: mark test as slow to run" + ) + +def pytest_collection_modifyitems(config, items): + if config.getoption("--runslow"): + return + skip_slow = pytest.mark.skip(reason="need --runslow option to run") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) + +# Provide a k8s client to interact with the integration test cluster +@pytest.fixture(scope='class') +def k8s_client(): + return k8s._get_k8s_api_client() diff --git a/test/e2e/replacement_values.py b/test/e2e/replacement_values.py new file mode 100644 index 0000000..a6e6f17 --- /dev/null +++ b/test/e2e/replacement_values.py @@ -0,0 +1,20 @@ +# 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. + +"""Stores the values used by each of the integration tests for replacing the +DynamoDB-specific test variables. +""" + +REPLACEMENT_VALUES = { + +} diff --git a/test/e2e/requirements.txt b/test/e2e/requirements.txt new file mode 100644 index 0000000..dbf18a0 --- /dev/null +++ b/test/e2e/requirements.txt @@ -0,0 +1 @@ +acktest @ git+https://github.com/aws-controllers-k8s/test-infra.git@9d5b3223a67683fb989b4f07f5b5e25aeffc2ba7 diff --git a/test/e2e/resources/table.yaml b/test/e2e/resources/table.yaml new file mode 100644 index 0000000..f15e1b3 --- /dev/null +++ b/test/e2e/resources/table.yaml @@ -0,0 +1,33 @@ +apiVersion: dynamodb.services.k8s.aws/v1alpha1 +kind: Table +metadata: + name: $TABLE_NAME +spec: + tableName: $TABLE_NAME + attributeDefinitions: + - attributeName: ForumName + attributeType: S + - attributeName: Subject + attributeType: S + - attributeName: LastPostDateTime + attributeType: S + keySchema: + - attributeName: ForumName + keyType: HASH + - attributeName: Subject + keyType: RANGE + localSecondaryIndexes: + - indexName: LastPostIndex + keySchema: + - attributeName: ForumName + keyType: HASH + - attributeName: LastPostDateTime + keyType: RANGE + projection: + projectionType: KEYS_ONLY + provisionedThroughput: + readCapacityUnits: 5 + writeCapacityUnits: 5 + streamSpecification: + streamEnabled: true + streamViewType: "NEW_AND_OLD_IMAGES" \ No newline at end of file diff --git a/test/e2e/service_bootstrap.py b/test/e2e/service_bootstrap.py new file mode 100644 index 0000000..c9c03e0 --- /dev/null +++ b/test/e2e/service_bootstrap.py @@ -0,0 +1,33 @@ +# 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. + +"""Bootstraps the resources required to run the DynamoDB integration tests. +""" + +import logging +from pathlib import Path + +from acktest import resources +from e2e import bootstrap_directory +from e2e.bootstrap_resources import TestBootstrapResources + + +def service_bootstrap() -> dict: + logging.getLogger().setLevel(logging.INFO) + + return TestBootstrapResources( + ).__dict__ + +if __name__ == "__main__": + config = service_bootstrap() + resources.write_bootstrap_config(config, bootstrap_directory) \ No newline at end of file diff --git a/test/e2e/service_cleanup.py b/test/e2e/service_cleanup.py new file mode 100644 index 0000000..5a5e53e --- /dev/null +++ b/test/e2e/service_cleanup.py @@ -0,0 +1,33 @@ +# 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. + +"""Cleans up the resources created by the DynamoDB bootstrapping process. +""" + +import logging +from pathlib import Path + +from acktest import resources +from e2e import bootstrap_directory +from e2e.bootstrap_resources import TestBootstrapResources + +def service_cleanup(config: dict): + logging.getLogger().setLevel(logging.INFO) + + resources = TestBootstrapResources( + **config + ) + +if __name__ == "__main__": + bootstrap_config = resources.read_bootstrap_config(bootstrap_directory) + service_cleanup(bootstrap_config) \ No newline at end of file diff --git a/test/e2e/tests/test_table.py b/test/e2e/tests/test_table.py new file mode 100644 index 0000000..2fe1428 --- /dev/null +++ b/test/e2e/tests/test_table.py @@ -0,0 +1,97 @@ +# 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. + +"""Integration tests for the DynamoDB Table API. +""" + +import boto3 +import pytest +import time +import logging +from typing import Dict, Tuple + +from acktest.resources import random_suffix_name +from acktest.k8s import resource as k8s +from e2e import service_marker, CRD_GROUP, CRD_VERSION, load_dynamodb_resource, wait_for_cr_status +from e2e.replacement_values import REPLACEMENT_VALUES + +RESOURCE_PLURAL = "tables" + +DELETE_WAIT_AFTER_SECONDS = 15 + +@pytest.fixture(scope="module") +def dynamodb_client(): + return boto3.client("dynamodb") + +@service_marker +@pytest.mark.canary +class TestTable: + def get_table(self, dynamodb_client, table_name: str) -> dict: + try: + resp = dynamodb_client.describe_table( + TableName=table_name, + ) + return resp["Table"] + + except Exception as e: + logging.debug(e) + return None + + def table_exists(self, dynamodb_client, table_name: str) -> bool: + return self.get_table(dynamodb_client, table_name) is not None + + def test_smoke(self, dynamodb_client): + resource_name = random_suffix_name("table", 32) + + replacements = REPLACEMENT_VALUES.copy() + replacements["TABLE_NAME"] = resource_name + + # Load Table CR + resource_data = load_dynamodb_resource( + "table", + 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) + cr = k8s.wait_resource_consumed_by_controller(ref) + + assert cr is not None + assert k8s.get_resource_exists(ref) + + wait_for_cr_status( + ref, + "tableStatus", + "ACTIVE", + 10, + 5, + ) + + # Check DynamoDB Table exists + exists = self.table_exists(dynamodb_client, resource_name) + assert exists + + # Delete k8s resource + _, deleted = k8s.delete_custom_resource(ref) + assert deleted is True + + time.sleep(DELETE_WAIT_AFTER_SECONDS) + + # Check DynamoDB Table doesn't exists + exists = self.table_exists(dynamodb_client, resource_name) + assert not exists \ No newline at end of file