Skip to content

Commit

Permalink
feat: implement cli for state-management
Browse files Browse the repository at this point in the history
  • Loading branch information
mcastellin authored Aug 25, 2023
1 parent e5d2e4a commit b8afcf8
Show file tree
Hide file tree
Showing 12 changed files with 436 additions and 156 deletions.
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions cmd/common.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
38 changes: 0 additions & 38 deletions cmd/commands.go → cmd/fail.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
59 changes: 55 additions & 4 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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 {
Expand All @@ -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",
Expand All @@ -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)
}
}
51 changes: 51 additions & 0 deletions cmd/recover.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
105 changes: 105 additions & 0 deletions cmd/state.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
26 changes: 26 additions & 0 deletions domain/errors.go
Original file line number Diff line number Diff line change
@@ -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()
}
Loading

0 comments on commit b8afcf8

Please sign in to comment.