From b8afcf8ba86ecfaf3adb4929e19681368e539de6 Mon Sep 17 00:00:00 2001 From: Manuel Castellin Date: Fri, 25 Aug 2023 12:47:11 +0200 Subject: [PATCH] feat: implement cli for state-management --- Makefile | 10 +- cmd/common.go | 24 ++++ cmd/{commands.go => fail.go} | 38 ------ cmd/main.go | 59 +++++++- cmd/recover.go | 51 +++++++ cmd/state.go | 105 +++++++++++++++ domain/errors.go | 26 ++++ domain/{main.go => types.go} | 21 ++- service/asg/selector.go | 2 +- service/ecs/selector.go | 2 +- state/manager.go | 252 ++++++++++++++++++++++------------- state/manager_test.go | 2 +- 12 files changed, 436 insertions(+), 156 deletions(-) create mode 100644 cmd/common.go rename cmd/{commands.go => fail.go} (73%) create mode 100644 cmd/recover.go create mode 100644 cmd/state.go create mode 100644 domain/errors.go rename domain/{main.go => types.go} (85%) diff --git a/Makefile b/Makefile index e59bb12..e260478 100644 --- a/Makefile +++ b/Makefile @@ -3,18 +3,18 @@ BUILD_VERSION=dev-snapshot clean: - go clean -testcache + @go clean -testcache test: - go test ./... -v + @go test ./... -v build: clean go build -ldflags="-X main.BuildVersion=$(BUILD_VERSION)" -o bin/aws-fail-az cmd/*.go install: build - mkdir -p ~/bin/ - cp bin/aws-fail-az ~/bin/aws-fail-az - chmod 700 ~/bin/aws-fail-az + @mkdir -p ~/bin/ + @cp bin/aws-fail-az ~/bin/aws-fail-az + @chmod 700 ~/bin/aws-fail-az # Auto-generate AWS api mocks for unit testing # IMPORTANT!! Run this target every time you need to modify the `domain` package diff --git a/cmd/common.go b/cmd/common.go new file mode 100644 index 0000000..560dc72 --- /dev/null +++ b/cmd/common.go @@ -0,0 +1,24 @@ +package main + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/mcastellin/aws-fail-az/awsapis" + "github.com/mcastellin/aws-fail-az/state" +) + +func getManager(namespace string) state.StateManager { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + log.Fatalf("Failed to load AWS configuration: %v", err) + } + + provider := awsapis.NewProviderFromConfig(&cfg) + + return &state.StateManagerImpl{ + Api: awsapis.NewDynamodbApi(&provider), + Namespace: namespace, + } +} diff --git a/cmd/commands.go b/cmd/fail.go similarity index 73% rename from cmd/commands.go rename to cmd/fail.go index fa3a9db..cc8c0d5 100644 --- a/cmd/commands.go +++ b/cmd/fail.go @@ -118,41 +118,3 @@ func FailCommand(namespace string, readFromStdin bool, configFile string) { } } } - -func RecoverCommand(namespace string) { - cfg, err := config.LoadDefaultConfig(context.TODO()) - if err != nil { - log.Fatalf("Failed to load AWS configuration: %v", err) - } - provider := awsapis.NewProviderFromConfig(&cfg) - - stateManager := &state.StateManagerImpl{ - Api: awsapis.NewDynamodbApi(&provider), - Namespace: namespace, - } - - stateManager.Initialize() - - states, err := stateManager.ReadStates() - if err != nil { - log.Panic(err) - } - for _, s := range states { - if s.ResourceType == ecs.RESOURCE_TYPE { - err = ecs.RestoreFromState(s.State, &provider) - } else if s.ResourceType == asg.RESOURCE_TYPE { - err = asg.RestoreFromState(s.State, &provider) - } else { - err = fmt.Errorf("Unknown resource of type %s found in state for key %s. Could not recover.\n", - s.ResourceType, - s.Key, - ) - } - - if err != nil { - log.Println(err) - } else { - stateManager.RemoveState(s) - } - } -} diff --git a/cmd/main.go b/cmd/main.go index ea74105..a3b7726 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -9,8 +9,11 @@ import ( var BuildVersion string = "" var ( - namespace string - stdin bool + stdin bool + namespace string + resourceType string + resourceKey string + resourceStateData string ) var rootCmd = &cobra.Command{ @@ -19,7 +22,7 @@ var rootCmd = &cobra.Command{ } var failCmd = &cobra.Command{ - Use: "fail", + Use: "fail [CONFIG_FILE]", Short: "Start AZ failure injection based on the provided configuration from stdin", Run: func(cmd *cobra.Command, args []string) { if !stdin && len(args) != 1 { @@ -43,6 +46,33 @@ var recoverCmd = &cobra.Command{ }, } +var stateSaveCmd = &cobra.Command{ + Use: "state-save", + Short: "Store a state object in Dynamodb", + Run: func(cmd *cobra.Command, args []string) { + if stdin && len(resourceStateData) > 0 { + log.Fatalf("State files are not supported when reading from stdin. Found %d.", len(args)) + } + SaveState(namespace, resourceType, resourceKey, stdin, resourceStateData) + }, +} + +var stateReadCmd = &cobra.Command{ + Use: "state-read", + Short: "Read a state object from Dynamodb", + Run: func(cmd *cobra.Command, args []string) { + ReadStates(namespace, resourceType, resourceKey) + }, +} + +var stateDeleteCmd = &cobra.Command{ + Use: "state-delete", + Short: "Delete a state object from Dynamodb", + Run: func(cmd *cobra.Command, args []string) { + DeleteState(namespace, resourceType, resourceKey) + }, +} + var versionCmd = &cobra.Command{ Use: "version", Short: "Print the command version", @@ -58,11 +88,32 @@ func main() { recoverCmd.Flags().StringVar(&namespace, "ns", "", "The namespace assigned to this operation. Used to uniquely identify resources state for recovery.") + stateSaveCmd.Flags().StringVar(&namespace, "ns", "", "The namespace assigned to this operation. Used to uniquely identify resources state for recovery.") + stateSaveCmd.Flags().StringVar(&resourceType, "type", "", "The type of resource state to store") + stateSaveCmd.Flags().StringVar(&resourceKey, "key", "", "A unique key to identify this resource") + stateSaveCmd.Flags().StringVar(&resourceStateData, "data", "", "The payload for the resource state as a string value") + stateSaveCmd.Flags().BoolVar(&stdin, "stdin", false, "Read resource state from stdin.") + stateSaveCmd.MarkFlagRequired("type") + stateSaveCmd.MarkFlagRequired("key") + + stateReadCmd.Flags().StringVar(&namespace, "ns", "", "The namespace assigned to this operation. Used to uniquely identify resources state for recovery.") + stateReadCmd.Flags().StringVar(&resourceType, "type", "", "Filter states by resource type") + stateReadCmd.Flags().StringVar(&resourceKey, "key", "", "Filter states by resource key") + + stateDeleteCmd.Flags().StringVar(&namespace, "ns", "", "The namespace assigned to this operation. Used to uniquely identify resources state for recovery.") + stateDeleteCmd.Flags().StringVar(&resourceType, "type", "", "Filter states by resource type") + stateDeleteCmd.Flags().StringVar(&resourceKey, "key", "", "Filter states by resource key") + stateDeleteCmd.MarkFlagRequired("type") + stateDeleteCmd.MarkFlagRequired("key") + rootCmd.AddCommand(failCmd) rootCmd.AddCommand(recoverCmd) rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(stateSaveCmd) + rootCmd.AddCommand(stateReadCmd) + rootCmd.AddCommand(stateDeleteCmd) if err := rootCmd.Execute(); err != nil { - log.Panic(err) + log.Fatal(err) } } diff --git a/cmd/recover.go b/cmd/recover.go new file mode 100644 index 0000000..2be5132 --- /dev/null +++ b/cmd/recover.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "fmt" + "log" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/mcastellin/aws-fail-az/awsapis" + "github.com/mcastellin/aws-fail-az/service/asg" + "github.com/mcastellin/aws-fail-az/service/ecs" + "github.com/mcastellin/aws-fail-az/state" +) + +func RecoverCommand(namespace string) { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + log.Fatalf("Failed to load AWS configuration: %v", err) + } + provider := awsapis.NewProviderFromConfig(&cfg) + + stateManager := &state.StateManagerImpl{ + Api: awsapis.NewDynamodbApi(&provider), + Namespace: namespace, + } + + stateManager.Initialize() + + states, err := stateManager.ReadStates(&state.QueryStatesInput{}) + if err != nil { + log.Panic(err) + } + for _, s := range states { + if s.ResourceType == ecs.RESOURCE_TYPE { + err = ecs.RestoreFromState(s.State, &provider) + } else if s.ResourceType == asg.RESOURCE_TYPE { + err = asg.RestoreFromState(s.State, &provider) + } else { + err = fmt.Errorf("Unknown resource of type %s found in state with key %s. Object will be ignored.\n", + s.ResourceType, + s.Key, + ) + } + + if err != nil { + log.Println(err) + } else { + stateManager.RemoveState(s) + } + } +} diff --git a/cmd/state.go b/cmd/state.go new file mode 100644 index 0000000..d00ab89 --- /dev/null +++ b/cmd/state.go @@ -0,0 +1,105 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "os" + + "github.com/mcastellin/aws-fail-az/state" +) + +type ReadStatesOutput struct { + Namespace string `json:"namespace"` + ResourceType string `json:"type"` + ResourceKey string `json:"key"` + State string `json:"state"` +} + +func SaveState(namespace string, + resourceType string, + resourceKey string, + readFromStdin bool, + stateData string) { + + var statePayload []byte + var err error + if readFromStdin { + statePayload, err = io.ReadAll(os.Stdin) + if err != nil { + log.Panic(err) + } + } else { + statePayload = []byte(stateData) + } + + if len(statePayload) == 0 { + log.Fatal("No data was provided to store in state. Exiting.") + } + + stateManager := getManager(namespace) + stateManager.Initialize() + + err = stateManager.Save(resourceType, resourceKey, statePayload) + if err != nil { + log.Fatal(err) + } +} + +func ReadStates(namespace string, resourceType string, resourceKey string) { + + // Discard logging to facilitate output parsing + log.SetOutput(io.Discard) + + stateManager := getManager(namespace) + stateManager.Initialize() + + states, err := stateManager.ReadStates(&state.QueryStatesInput{ + ResourceType: resourceType, + ResourceKey: resourceKey, + }) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + stateData := []ReadStatesOutput{} + for _, s := range states { + stateData = append(stateData, + ReadStatesOutput{ + Namespace: s.Namespace, + ResourceType: s.ResourceType, + ResourceKey: s.ResourceKey, + State: string(s.State), + }) + } + + if len(states) > 0 { + stateJson, err := json.Marshal(stateData) + if err != nil { + fmt.Println("Error unmarshalling state object. Exiting.") + } + fmt.Println(string(stateJson)) + } else { + fmt.Println("[]") + } +} + +func DeleteState(namespace string, resourceType string, resourceKey string) { + + stateManager := getManager(namespace) + stateManager.Initialize() + + result, err := stateManager.GetState(resourceType, resourceKey) + if err != nil { + log.Fatal(err) + } + + err = stateManager.RemoveState(*result) + if err != nil { + log.Fatalf("Error removing state object with key %s", result.Key) + } else { + log.Printf("State with key %s removed successfully", result.Key) + } +} diff --git a/domain/errors.go b/domain/errors.go new file mode 100644 index 0000000..f74ff34 --- /dev/null +++ b/domain/errors.go @@ -0,0 +1,26 @@ +package domain + +// An error type to signal the activity has failed and whether or not +// it can be retried +type ActivityFailedError struct { + Wrap error + Temporary bool +} + +func (e ActivityFailedError) Error() string { + return e.Wrap.Error() +} + +func (e ActivityFailedError) IsTemporary() bool { + return e.Temporary +} + +// An error type to signal the current activity has failed and that the +// program execution should be interrupted as soon as possible +type InterruptExecutionError struct { + Wrap error +} + +func (e InterruptExecutionError) Error() string { + return e.Wrap.Error() +} diff --git a/domain/main.go b/domain/types.go similarity index 85% rename from domain/main.go rename to domain/types.go index e595660..d6486ce 100644 --- a/domain/main.go +++ b/domain/types.go @@ -21,6 +21,12 @@ type FaultConfiguration struct { Services []ServiceSelector `json:"services"` } +// AWS Tag +type AWSTag struct { + Name string `json:"Name"` + Value string `json:"Value"` +} + // AWS ServiceSelector type ServiceSelector struct { Type string `json:"type"` @@ -28,22 +34,13 @@ type ServiceSelector struct { Tags []AWSTag `json:"tags"` } -// AWS Tag -type AWSTag struct { - Name string `json:"Name"` - Value string `json:"Value"` -} - // Validates all required fields for service selector have been provided -func ValidateServiceSelector(selector ServiceSelector) error { - - if selector.Filter != "" && len(selector.Tags) > 0 { +func (s ServiceSelector) Validate() error { + if s.Filter != "" && len(s.Tags) > 0 { return fmt.Errorf("Validation failed: Both 'filter' and 'tags' selectors specified. Only one allowed.") } - - if selector.Filter == "" && len(selector.Tags) == 0 { + if s.Filter == "" && len(s.Tags) == 0 { return fmt.Errorf("Validation failed: One of 'filter' and 'tags' selectors must be specified.") } - return nil } diff --git a/service/asg/selector.go b/service/asg/selector.go index fb7e89e..807088b 100644 --- a/service/asg/selector.go +++ b/service/asg/selector.go @@ -34,7 +34,7 @@ func NewFromConfig(selector domain.ServiceSelector, provider *awsapis.AWSProvide var asgNames []string var err error - err = domain.ValidateServiceSelector(selector) + err = selector.Validate() if err != nil { return nil, err } diff --git a/service/ecs/selector.go b/service/ecs/selector.go index 1e30c0a..ac491a3 100644 --- a/service/ecs/selector.go +++ b/service/ecs/selector.go @@ -35,7 +35,7 @@ func NewFromConfig(selector domain.ServiceSelector, provider *awsapis.AWSProvide objs := []domain.ConsistentStateService{} var err error - err = domain.ValidateServiceSelector(selector) + err = selector.Validate() if err != nil { return nil, err } diff --git a/state/manager.go b/state/manager.go index dc82ca2..0fa3f96 100644 --- a/state/manager.go +++ b/state/manager.go @@ -17,15 +17,43 @@ import ( ) // The default table name to store resource states -const FALLBACK_STATE_TABLE_NAME string = "aws-fail-az-state" +const FALLBACK_STATE_TABLE_NAME string = "aws-fail-az-state-table" type StateManager interface { Initialize() Save(resourceType string, resourceKey string, state []byte) error - ReadStates() ([]ResourceState, error) + GetState(resourceType string, resourceKey string) (*ResourceState, error) + ReadStates(params *QueryStatesInput) ([]ResourceState, error) RemoveState(stateObj ResourceState) error } +// Query states input struct +type QueryStatesInput struct { + ResourceType string + ResourceKey string +} + +// appends filter conditions to expression builder for building state query +func (q QueryStatesInput) filterExpression(builder expression.Builder) expression.Builder { + exprList := []expression.ConditionBuilder{} + if q.ResourceKey != "" { + nameExpr := expression.Name("resourceKey").Equal(expression.Value(q.ResourceKey)) + exprList = append(exprList, nameExpr) + } + if q.ResourceType != "" { + nameExpr := expression.Name("resourceType").Equal(expression.Value(q.ResourceType)) + exprList = append(exprList, nameExpr) + } + + if len(exprList) > 1 { + builder = builder.WithFilter(expression.And(exprList[0], exprList[1])) + } else if len(exprList) > 0 { + builder = builder.WithFilter(exprList[0]) + } + + return builder +} + type ResourceState struct { Namespace string `dynamodbav:"namespace"` Key string `dynamodbav:"key"` @@ -54,99 +82,13 @@ type StateManagerImpl struct { Namespace string } -// Check if the state table already exists for the current AWS Account/Region -// Returns: true if the table exists, false otherwise -func (m StateManagerImpl) tableExists() (bool, error) { - input := &dynamodb.DescribeTableInput{ - TableName: aws.String(m.TableName), - } - - _, err := m.Api.DescribeTable(context.TODO(), input) - if err != nil { - if t := new(types.ResourceNotFoundException); errors.As(err, &t) { - return false, nil // Table does not exists - } - return false, err // Other error occurred - } - - return true, nil // Table exists -} - -// Creates the resource state table in Dynamodb for the current AWS Account/Region -// and wait for table creationg before returning -func (m StateManagerImpl) createTable() (*dynamodb.CreateTableOutput, error) { - input := &dynamodb.CreateTableInput{ - TableName: aws.String(m.TableName), - KeySchema: []types.KeySchemaElement{ - { - AttributeName: aws.String("namespace"), - KeyType: types.KeyTypeHash, - }, { - AttributeName: aws.String("key"), - KeyType: types.KeyTypeRange, - }, - }, - AttributeDefinitions: []types.AttributeDefinition{ - { - AttributeName: aws.String("namespace"), - AttributeType: types.ScalarAttributeTypeS, - }, { - AttributeName: aws.String("key"), - AttributeType: types.ScalarAttributeTypeS, - }, { - AttributeName: aws.String("createdTime"), - AttributeType: types.ScalarAttributeTypeN, - }, - }, - LocalSecondaryIndexes: []types.LocalSecondaryIndex{ - { - IndexName: aws.String("LSINamespace"), - KeySchema: []types.KeySchemaElement{ - { - AttributeName: aws.String("namespace"), - KeyType: types.KeyTypeHash, - }, { - AttributeName: aws.String("createdTime"), - KeyType: types.KeyTypeRange, - }, - }, - Projection: &types.Projection{ - ProjectionType: types.ProjectionTypeAll, - }, - }, - }, - ProvisionedThroughput: &types.ProvisionedThroughput{ - ReadCapacityUnits: aws.Int64(1), - WriteCapacityUnits: aws.Int64(1), - }, - } - - createOutput, err := m.Api.CreateTable(context.TODO(), input) - if err != nil { - log.Fatalf("Failed to create Dynamodb Table to store the current resource state, %v", err) - } - - log.Printf("Wait for table exists: %s", m.TableName) - waiter := m.Api.NewTableExistsWaiter() - err = waiter.Wait( - context.TODO(), - &dynamodb.DescribeTableInput{TableName: aws.String(m.TableName)}, - 5*time.Minute, - ) - if err != nil { - log.Fatalf("Wait for table exists failed. It's not safe to continue this operation. %v", err) - } - - return createOutput, nil -} - // Initialize the state manager. // This only needs to be called once at the beginning of the program to create the // state table in Dynamodb. Further calls will have no effect. func (m *StateManagerImpl) Initialize() { - stateTableName := os.Getenv("AWS_FAILAZ_STATE_TABLE") + stateTableName := os.Getenv("AWS_FAIL_AZ_STATE_TABLE") if stateTableName == "" { - log.Printf("AWS_FAILAZ_STATE_STABLE variable is not set. Using default %s", FALLBACK_STATE_TABLE_NAME) + log.Printf("AWS_FAIL_AZ_STATE_TABLE variable is not set. Using default %s", FALLBACK_STATE_TABLE_NAME) m.TableName = FALLBACK_STATE_TABLE_NAME } else { m.TableName = stateTableName @@ -171,8 +113,7 @@ func (m *StateManagerImpl) Initialize() { } func (m StateManagerImpl) Save(resourceType string, resourceKey string, state []byte) error { - - key := fmt.Sprintf("/%s/%s/%s", m.Namespace, resourceType, resourceKey) + key := m.fullKey(resourceType, resourceKey) stateObj := ResourceState{ Namespace: m.Namespace, Key: key, @@ -210,10 +151,41 @@ func (m StateManagerImpl) Save(resourceType string, resourceKey string, state [] return nil } -func (m StateManagerImpl) ReadStates() ([]ResourceState, error) { +func (m StateManagerImpl) GetState(resourceType string, resourceKey string) (*ResourceState, error) { + key := m.fullKey(resourceType, resourceKey) + stateObj := ResourceState{ + Namespace: m.Namespace, + Key: key, + ResourceKey: resourceKey, + ResourceType: resourceType, + } + + getItemInput := &dynamodb.GetItemInput{ + TableName: aws.String(m.TableName), + Key: stateObj.GetKey(), + } + response, err := m.Api.GetItem(context.TODO(), getItemInput) + if err != nil { + return nil, err + } + if len(response.Item) == 0 { + return nil, fmt.Errorf("Unknown state key %s", key) + } + + var out ResourceState + err = attributevalue.UnmarshalMap(response.Item, &out) + if err != nil { + return nil, fmt.Errorf("Error unmarshalling resource state.") + } + return &out, nil +} +func (m StateManagerImpl) ReadStates(params *QueryStatesInput) ([]ResourceState, error) { keyExpr := expression.Key("namespace").Equal(expression.Value(m.Namespace)) - expr, err := expression.NewBuilder().WithKeyCondition(keyExpr).Build() + builder := expression.NewBuilder().WithKeyCondition(keyExpr) + builder = params.filterExpression(builder) + + expr, err := builder.Build() if err != nil { log.Println("Unable to build query expression to fetch resource states") return []ResourceState{}, err @@ -225,9 +197,11 @@ func (m StateManagerImpl) ReadStates() ([]ResourceState, error) { TableName: aws.String(m.TableName), IndexName: aws.String("LSINamespace"), KeyConditionExpression: expr.KeyCondition(), + FilterExpression: expr.Filter(), ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), } + paginator := m.Api.NewQueryPaginator(queryInput) for paginator.HasMorePages() { queryOutput, err := paginator.NextPage(context.TODO()) @@ -260,3 +234,93 @@ func (m StateManagerImpl) RemoveState(stateObj ResourceState) error { return nil } + +// Check if the state table already exists for the current AWS Account/Region +// Returns: true if the table exists, false otherwise +func (m StateManagerImpl) tableExists() (bool, error) { + input := &dynamodb.DescribeTableInput{ + TableName: aws.String(m.TableName), + } + + _, err := m.Api.DescribeTable(context.TODO(), input) + if err != nil { + var t *types.ResourceNotFoundException + if errors.As(err, &t) { + return false, nil // Table does not exists + } + return false, err // Other error occurred + } + return true, nil // Table exists +} + +// Creates the resource state table in Dynamodb for the current AWS Account/Region +// and wait for table creationg before returning +func (m StateManagerImpl) createTable() (*dynamodb.CreateTableOutput, error) { + input := &dynamodb.CreateTableInput{ + TableName: aws.String(m.TableName), + KeySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String("namespace"), + KeyType: types.KeyTypeHash, + }, { + AttributeName: aws.String("key"), + KeyType: types.KeyTypeRange, + }, + }, + AttributeDefinitions: []types.AttributeDefinition{ + { + AttributeName: aws.String("namespace"), + AttributeType: types.ScalarAttributeTypeS, + }, { + AttributeName: aws.String("key"), + AttributeType: types.ScalarAttributeTypeS, + }, { + AttributeName: aws.String("createdTime"), + AttributeType: types.ScalarAttributeTypeN, + }, + }, + LocalSecondaryIndexes: []types.LocalSecondaryIndex{ + { + IndexName: aws.String("LSINamespace"), + KeySchema: []types.KeySchemaElement{ + { + AttributeName: aws.String("namespace"), + KeyType: types.KeyTypeHash, + }, { + AttributeName: aws.String("createdTime"), + KeyType: types.KeyTypeRange, + }, + }, + Projection: &types.Projection{ + ProjectionType: types.ProjectionTypeAll, + }, + }, + }, + ProvisionedThroughput: &types.ProvisionedThroughput{ + ReadCapacityUnits: aws.Int64(1), + WriteCapacityUnits: aws.Int64(1), + }, + } + + createOutput, err := m.Api.CreateTable(context.TODO(), input) + if err != nil { + log.Fatalf("Failed to create Dynamodb Table to store the current resource state, %v", err) + } + + log.Printf("Wait for table exists: %s", m.TableName) + waiter := m.Api.NewTableExistsWaiter() + err = waiter.Wait( + context.TODO(), + &dynamodb.DescribeTableInput{TableName: aws.String(m.TableName)}, + 5*time.Minute, + ) + if err != nil { + log.Fatalf("Wait for table exists failed. It's not safe to continue this operation. %v", err) + } + + return createOutput, nil +} + +func (m StateManagerImpl) fullKey(resourceType string, resourceKey string) string { + return fmt.Sprintf("/%s/%s/%s", m.Namespace, resourceType, resourceKey) +} diff --git a/state/manager_test.go b/state/manager_test.go index 634083c..2db9cba 100644 --- a/state/manager_test.go +++ b/state/manager_test.go @@ -16,7 +16,7 @@ import ( func TestStateInitializeNewTableWithOsVar(t *testing.T) { - t.Setenv("AWS_FAILAZ_STATE_TABLE", "test-value") + t.Setenv("AWS_FAIL_AZ_STATE_TABLE", "test-value") ctrl, _ := gomock.WithContext(context.Background(), t) defer ctrl.Finish()