diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index c4401fa..4891f84 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
go-version:
- - "1.21.x"
+ - "1.23.x"
os:
- "ubuntu-latest"
steps:
diff --git a/.gitignore b/.gitignore
index 0880449..58772d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,5 @@ deco.json
decofile-prod.json
docker-compose.yml
# Fresh
-tmp/runner-build
\ No newline at end of file
+tmp/runner-build
+./docs/*
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0c9a5ec
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,109 @@
+.PHONY: all build run test clean lint dev docker-build docker-run help
+
+# Go related variables
+BINARY_NAME=ec2apigo
+MAIN_FILE=main.go
+BUILD_DIR=./tmp
+
+# Docker related variables
+DOCKER_IMAGE=ec2apigo
+DOCKER_TAG=latest
+
+# Go build flags
+LDFLAGS=-ldflags "-w -s"
+
+# Default target
+all: build
+
+# Build the application
+build:
+ @echo "Building $(BINARY_NAME)..."
+ @go build -o $(BUILD_DIR)/$(BINARY_NAME) $(LDFLAGS) $(MAIN_FILE)
+
+# Run the application
+run:
+ @go run $(MAIN_FILE)
+
+# Run with live reload using air
+dev:
+ @echo "Starting development server with air..."
+ @air
+
+# Run tests
+test:
+ @echo "Running tests..."
+ @go test -v ./...
+
+# Run tests with coverage
+test-coverage:
+ @echo "Running tests with coverage..."
+ @go test -v -cover ./...
+
+# Clean build artifacts
+clean:
+ @echo "Cleaning..."
+ @rm -rf $(BUILD_DIR)
+ @go clean
+ @echo "Cleaned build cache"
+
+# Run go fmt
+fmt:
+ @echo "Running go fmt..."
+ @go fmt ./...
+
+# Run go vet
+vet:
+ @echo "Running go vet..."
+ @go vet ./...
+
+# Install dependencies
+deps:
+ @echo "Installing dependencies..."
+ @go mod download
+ @go mod tidy
+
+# Run linter
+lint:
+ @echo "Running linter..."
+ @if command -v golangci-lint >/dev/null; then \
+ golangci-lint run; \
+ else \
+ echo "golangci-lint is not installed. Installing..."; \
+ go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest; \
+ golangci-lint run; \
+ fi
+
+# Build Docker image
+docker-build:
+ @echo "Building Docker image..."
+ @docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) -f docker/Dockerfile .
+
+# Run Docker container
+docker-run:
+ @echo "Running Docker container..."
+ @docker run -p 8080:8080 $(DOCKER_IMAGE):$(DOCKER_TAG)
+
+# Install development tools
+tools:
+ @echo "Installing development tools..."
+ @go install github.com/air-verse/air@latest
+ @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
+
+# Display help information
+help:
+ @echo "Available targets:"
+ @echo " all - Build the application (default)"
+ @echo " build - Build the application"
+ @echo " run - Run the application"
+ @echo " dev - Run the application with live reload using air"
+ @echo " test - Run tests"
+ @echo " test-coverage - Run tests with coverage"
+ @echo " clean - Clean build artifacts"
+ @echo " fmt - Run go fmt"
+ @echo " vet - Run go vet"
+ @echo " deps - Install dependencies"
+ @echo " lint - Run linter"
+ @echo " tools - Install development tools"
+ @echo " docker-build - Build Docker image"
+ @echo " docker-run - Run Docker container"
+ @echo " help - Display this help message"
\ No newline at end of file
diff --git a/api/handlers.go b/api/handlers.go
index 608db70..495a339 100644
--- a/api/handlers.go
+++ b/api/handlers.go
@@ -25,7 +25,7 @@ import (
log "github.com/sirupsen/logrus"
)
-// PingHandler responds to ping requests
+// PingHandler Ping Returns a response of pong if the application is up and running
func (s *server) PingHandler(w http.ResponseWriter, r *http.Request) {
w = LogWriter{w}
log.Debug("Ping/Pong")
diff --git a/api/handlers_resourcegroups.go b/api/handlers_resourcegroups.go
new file mode 100644
index 0000000..540d22a
--- /dev/null
+++ b/api/handlers_resourcegroups.go
@@ -0,0 +1,180 @@
+/*
+Copyright © 2021 Yale University
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+package api
+
+import (
+ "encoding/json"
+ "github.com/gorilla/mux"
+ log "github.com/sirupsen/logrus"
+ "net/http"
+
+ "github.com/YaleSpinup/apierror"
+ rg2 "github.com/YaleSpinup/ec2-api/resourcegroups"
+ "github.com/aws/aws-sdk-go/service/resourcegroups"
+)
+
+// ResourceGroupsCreateHandler handles the creation of a new resource group
+func (s *server) ResourceGroupsCreateHandler(w http.ResponseWriter, r *http.Request) {
+ w = LogWriter{w}
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+
+ vars := mux.Vars(r)
+ account := vars["account"]
+
+ log.Debugf("Creating resource group in account: %s", account)
+
+ // Initialize the resource groups service for this account
+ if err := s.setResourceGroupsService(account); err != nil {
+ log.Errorf("Failed to initialize resource groups service: %v", err)
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to initialize resource groups service", err))
+ return
+ }
+
+ // Updated input struct to match AWS SDK expectations
+ var input struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Query struct {
+ Type string `json:"Type"`
+ Query string `json:"Query"`
+ } `json:"query"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
+ handleError(w, apierror.New(apierror.ErrBadRequest, "invalid json input", err))
+ return
+ }
+
+ if input.Name == "" {
+ handleError(w, apierror.New(apierror.ErrBadRequest, "name is required", nil))
+ return
+ }
+
+ // Convert the input to AWS SDK format
+ createInput := rg2.CreateGroupInput{
+ Name: input.Name,
+ Description: input.Description,
+ ResourceQuery: &resourcegroups.ResourceQuery{
+ Type: &input.Query.Type,
+ Query: &input.Query.Query,
+ },
+ }
+
+ group, err := s.resourceGroups.CreateGroup(createInput)
+ if err != nil {
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to create resource group", err))
+ return
+ }
+
+ handleResponseOk(w, group)
+}
+
+// ResourceGroupsListHandler lists all resource groups
+func (s *server) ResourceGroupsListHandler(w http.ResponseWriter, r *http.Request) {
+ w = LogWriter{w}
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+
+ vars := mux.Vars(r)
+ account := vars["account"]
+
+ log.Debugf("Listing resource groups in account: %s", account)
+
+ if err := s.setResourceGroupsService(account); err != nil {
+ log.Errorf("Failed to initialize resource groups service: %v", err)
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to initialize resource groups service", err))
+ return
+ }
+
+ groups, err := s.resourceGroups.ListGroups()
+ if err != nil {
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to list resource groups", err))
+ return
+ }
+
+ log.Infof("Successfully listed resource groups for account: %s", account)
+ handleResponseOk(w, groups)
+}
+
+// ResourceGroupsGetHandler gets details of a specific resource group and its resources
+func (s *server) ResourceGroupsGetHandler(w http.ResponseWriter, r *http.Request) {
+ w = LogWriter{w}
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+
+ vars := mux.Vars(r)
+ account := vars["account"]
+ groupName := vars["id"]
+
+ log.Debugf("Getting resource group %s in account: %s", groupName, account)
+
+ if err := s.setResourceGroupsService(account); err != nil {
+ log.Errorf("Failed to initialize resource groups service: %v", err)
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to initialize resource groups service", err))
+ return
+ }
+
+ // Get group details
+ group, err := s.resourceGroups.GetGroup(groupName)
+ if err != nil {
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to get resource group", err))
+ return
+ }
+
+ // Get resources in the group
+ resources, err := s.resourceGroups.ListGroupResources(groupName)
+ if err != nil {
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to list group resources", err))
+ return
+ }
+
+ // Combine group details and resources
+ response := struct {
+ Group *resourcegroups.Group `json:"Group"`
+ Resources interface{} `json:"Resources"`
+ }{
+ Group: group,
+ Resources: resources,
+ }
+
+ log.Infof("Successfully retrieved resource group: %s", groupName)
+ handleResponseOk(w, response)
+}
+
+// ResourceGroupsDeleteHandler handles deletion of a resource group
+func (s *server) ResourceGroupsDeleteHandler(w http.ResponseWriter, r *http.Request) {
+ w = LogWriter{w}
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+
+ vars := mux.Vars(r)
+ account := vars["account"]
+ groupName := vars["id"]
+
+ log.Debugf("Deleting resource group %s in account: %s", groupName, account)
+
+ if err := s.setResourceGroupsService(account); err != nil {
+ log.Errorf("Failed to initialize resource groups service: %v", err)
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to initialize resource groups service", err))
+ return
+ }
+
+ if err := s.resourceGroups.DeleteGroup(groupName); err != nil {
+ handleError(w, apierror.New(apierror.ErrBadRequest, "failed to delete resource group", err))
+ return
+ }
+
+ log.Infof("Successfully deleted resource group: %s", groupName)
+ handleResponseOk(w, nil)
+}
diff --git a/api/orchestration_resourcegroups.go b/api/orchestration_resourcegroups.go
new file mode 100644
index 0000000..30f2c25
--- /dev/null
+++ b/api/orchestration_resourcegroups.go
@@ -0,0 +1,84 @@
+/*
+Copyright © 2021 Yale University
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
+package api
+
+import (
+ "fmt"
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/sts"
+
+ "github.com/YaleSpinup/ec2-api/resourcegroups"
+ log "github.com/sirupsen/logrus"
+)
+
+func (s *server) resourceGroupsServiceForAccount(account string) (*resourcegroups.ResourceGroups, error) {
+ log.Debugf("getting resourcegroups service for account %s", account)
+
+ accountNumber := s.mapAccountNumber(account)
+ if accountNumber == "" {
+ return nil, fmt.Errorf("account number not found for %s", account)
+ }
+
+ // Construct the role ARN for the target account
+ roleArn := fmt.Sprintf("arn:aws:iam::%s:role/SpinupPlusXAManagementRoleTst", accountNumber)
+ log.Debugf("assuming role: %s", roleArn)
+
+ // Use the existing session to create STS client
+ stsClient := sts.New(s.session.Session)
+
+ // Create the assume role input
+ assumeRoleInput := &sts.AssumeRoleInput{
+ RoleArn: aws.String(roleArn),
+ RoleSessionName: aws.String("resource-groups-session"),
+ }
+
+ // Add ExternalId if it exists
+ if s.session.ExternalID != "" {
+ assumeRoleInput.ExternalId = aws.String(s.session.ExternalID)
+ }
+
+ // Assume the role
+ roleOutput, err := stsClient.AssumeRole(assumeRoleInput)
+ if err != nil {
+ return nil, fmt.Errorf("failed to assume role: %v", err)
+ }
+
+ // Create a new session with the assumed role credentials
+ assumedSession := session.Must(session.NewSession(&aws.Config{
+ Credentials: credentials.NewStaticCredentials(
+ *roleOutput.Credentials.AccessKeyId,
+ *roleOutput.Credentials.SecretAccessKey,
+ *roleOutput.Credentials.SessionToken,
+ ),
+ Region: aws.String("us-east-1"), // or get this from config
+ }))
+
+ return resourcegroups.New(resourcegroups.WithSession(assumedSession)), nil
+}
+
+// setResourceGroupsService sets the resourcegroups service in the server
+func (s *server) setResourceGroupsService(account string) error {
+ rg, err := s.resourceGroupsServiceForAccount(account)
+ if err != nil {
+ return err
+ }
+
+ s.resourceGroups = rg
+ return nil
+}
diff --git a/api/orchestration_snapshot.go b/api/orchestration_snapshot.go
index d4d7248..a55de14 100644
--- a/api/orchestration_snapshot.go
+++ b/api/orchestration_snapshot.go
@@ -87,4 +87,3 @@ func (o *ec2Orchestrator) listSnapshots(ctx context.Context, perPage int64, page
return out.Snapshots, out.NextToken, nil
}
-
diff --git a/api/routes.go b/api/routes.go
index 493ea27..47d3e41 100644
--- a/api/routes.go
+++ b/api/routes.go
@@ -50,6 +50,8 @@ func (s *server) routes() {
api.HandleFunc("/{account}/volumes/{id}", s.VolumeGetHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/volumes/{id}/modifications", s.VolumeListModificationsHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/volumes/{id}/snapshots", s.VolumeListSnapshotsHandler).Methods(http.MethodGet)
+ api.HandleFunc("/{account}/resourcegroups", s.ResourceGroupsListHandler).Methods(http.MethodGet)
+ api.HandleFunc("/{account}/resourcegroups/{id}", s.ResourceGroupsGetHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/snapshots", s.SnapshotListHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/snapshots/synctags", s.SnapshotSyncTagHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/snapshots/{id}", s.SnapshotGetHandler).Methods(http.MethodGet)
@@ -64,6 +66,7 @@ func (s *server) routes() {
api.HandleFunc("/{account}/instances", s.InstanceCreateHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/instances/{id}/volumes", s.VolumeAttachHandler).Methods(http.MethodPost)
+ api.HandleFunc("/{account}/resourcegroups", s.ResourceGroupsCreateHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/sgs", s.SecurityGroupCreateHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/ssm/association", s.SSMAssociationByTagHandler).Methods(http.MethodPost)
api.HandleFunc("/{account}/volumes", s.VolumeCreateHandler).Methods(http.MethodPost)
@@ -77,6 +80,7 @@ func (s *server) routes() {
api.HandleFunc("/{account}/instances/{id}/ssm/association", s.InstanceSSMAssociationHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/instances/{id}/tags", s.InstanceUpdateHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/instances/{id}/attribute", s.InstanceUpdateHandler).Methods(http.MethodPut)
+ //api.HandleFunc("/{account}/resourcegroups/{id}", s.ResourceGroupsUpdateHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/sgs/{id}", s.SecurityGroupUpdateHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/sgs/{id}/tags", s.SecurityGroupUpdateHandler).Methods(http.MethodPut)
api.HandleFunc("/{account}/volumes/{id}", s.VolumeUpdateHandler).Methods(http.MethodPut)
@@ -87,6 +91,7 @@ func (s *server) routes() {
api.HandleFunc("/{account}/instanceprofiles/{name}", s.InstanceProfileDeleteHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/instanceprofiles/{name}", s.InstanceProfileGetHandler).Methods(http.MethodGet)
api.HandleFunc("/{account}/instanceprofiles/{name}", s.InstanceProfileCopyHandler).Methods(http.MethodPost)
+ api.HandleFunc("/{account}/resourcegroups/{id}", s.ResourceGroupsDeleteHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/sgs/{id}", s.SecurityGroupDeleteHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/volumes/{id}", s.VolumeDeleteHandler).Methods(http.MethodDelete)
api.HandleFunc("/{account}/snapshots/{id}", s.SnapshotDeleteHandler).Methods(http.MethodDelete)
diff --git a/api/server.go b/api/server.go
index 35bf963..fdb0e7b 100644
--- a/api/server.go
+++ b/api/server.go
@@ -19,6 +19,7 @@ package api
import (
"context"
"errors"
+ "github.com/YaleSpinup/ec2-api/resourcegroups"
"math/rand"
"net/http"
"os"
@@ -54,15 +55,16 @@ type proxyBackend struct {
}
type server struct {
- router *mux.Router
- version *apiVersion
- context context.Context
- session session.Session
- sessionCache *cache.Cache
- backend *proxyBackend
- accountsMap map[string]string
- orgPolicy string
- org string
+ router *mux.Router
+ version *apiVersion
+ context context.Context
+ session session.Session
+ sessionCache *cache.Cache
+ backend *proxyBackend
+ accountsMap map[string]string
+ orgPolicy string
+ org string
+ resourceGroups *resourcegroups.ResourceGroups
}
// NewServer creates a new server and starts it
@@ -118,7 +120,6 @@ func NewServer(config common.Config) error {
"/v2/ec2/version": "public",
"/v2/ec2/metrics": "public",
}
-
// load routes
s.routes()
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 9f6d7c4..0243009 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,5 +1,5 @@
# build stage
-FROM golang:alpine3.18 AS build-env
+FROM golang:alpine3.21 AS build-env
ARG version="0.0.0"
ARG prerelease=""
diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local
index d7d11d9..48985fa 100644
--- a/docker/Dockerfile.local
+++ b/docker/Dockerfile.local
@@ -1,5 +1,5 @@
# build stage
-FROM golang:alpine3.18 AS build-env
+FROM golang:alpine3.21 AS build-env
RUN apk add --no-cache git openssh-client gcc musl-dev
RUN mkdir /app
WORKDIR /app
diff --git a/go.mod b/go.mod
index 8592e8d..f8018ba 100644
--- a/go.mod
+++ b/go.mod
@@ -1,12 +1,12 @@
module github.com/YaleSpinup/ec2-api
-go 1.21
+go 1.23
require (
github.com/YaleSpinup/apierror v0.1.5
github.com/YaleSpinup/aws-go v0.2.7
- github.com/aws/amazon-ec2-instance-selector/v2 v2.4.1
- github.com/aws/aws-sdk-go v1.55.5
+ github.com/aws/amazon-ec2-instance-selector/v2 v2.0.3
+ github.com/aws/aws-sdk-go v1.55.6
github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1
@@ -14,41 +14,25 @@ require (
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.20.5
github.com/sirupsen/logrus v1.9.3
- golang.org/x/crypto v0.31.0
+ golang.org/x/crypto v0.32.0
)
require (
- github.com/atotto/clipboard v0.1.4 // indirect
- github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
- github.com/charmbracelet/bubbles v0.13.0 // indirect
- github.com/charmbracelet/bubbletea v0.21.0 // indirect
- github.com/charmbracelet/lipgloss v0.6.0 // indirect
- github.com/containerd/console v1.0.3 // indirect
- github.com/evertras/bubble-table v0.15.2 // indirect
- github.com/felixge/httpsnoop v1.0.3 // indirect
- github.com/imdario/mergo v0.3.12 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/ghodss/yaml v1.0.0 // indirect
+ github.com/imdario/mergo v0.3.16 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
- github.com/klauspost/compress v1.17.9 // indirect
- github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
- github.com/mattn/go-isatty v0.0.19 // indirect
- github.com/mattn/go-runewidth v0.0.15 // indirect
- github.com/mitchellh/go-homedir v1.1.0 // indirect
- github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
- github.com/muesli/cancelreader v0.2.2 // indirect
- github.com/muesli/reflow v0.3.0 // indirect
- github.com/muesli/termenv v0.15.2 // indirect
+ github.com/klauspost/compress v1.17.11 // indirect
+ github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
- github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
- github.com/prometheus/common v0.55.0 // indirect
+ github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
- github.com/rivo/uniseg v0.4.4 // indirect
- github.com/sahilm/fuzzy v0.1.0 // indirect
- go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
- golang.org/x/term v0.27.0 // indirect
- google.golang.org/protobuf v1.34.2 // indirect
+ github.com/rogpeppe/go-internal v1.11.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ google.golang.org/protobuf v1.36.4 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index c61a54c..ad37dd2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,130 +1,216 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/YaleSpinup/apierror v0.1.5 h1:ZW59fFo+bO30GpQ3TvGNBlY7kdX8L9KIEkaztsKnwiY=
github.com/YaleSpinup/apierror v0.1.5/go.mod h1:u2smW7kQNefbVbBpNNvHQj9TjLZdVC+fKxwKNtLIhb4=
github.com/YaleSpinup/aws-go v0.2.7 h1:IOV77Y7IF4DaRCOCtsdJMDjyIbPP3MawZRbedNk5YXQ=
github.com/YaleSpinup/aws-go v0.2.7/go.mod h1:O5e9MepqWMV7fmRPa9E4IIUKaiWCmdHWsSSFmKKvq7Y=
-github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
-github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
-github.com/aws/amazon-ec2-instance-selector/v2 v2.4.1 h1:DmxtwV+pkakkVRhxKcAgnLbxCxvT7k8DBG271dfKPZ8=
-github.com/aws/amazon-ec2-instance-selector/v2 v2.4.1/go.mod h1:AEJrtkLkCkfIBIazidrVrgZqaXl+9dxI/wRgjdw+7G0=
-github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
-github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
-github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
-github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/aws/amazon-ec2-instance-selector/v2 v2.0.3 h1:Rf2Wida+XfwS0NRuRLoAcFpmehXzNjEg0kd/PoACFBE=
+github.com/aws/amazon-ec2-instance-selector/v2 v2.0.3/go.mod h1:oCl6ho82TvIbrUfTld62nIkRBao22lUKEIMSmpVZDFY=
+github.com/aws/aws-sdk-go v1.38.27/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
+github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/charmbracelet/bubbles v0.13.0 h1:zP/ROH3wJEBqZWKIsD50ZKKlx3ydLInq3LdD/Nrlb8w=
-github.com/charmbracelet/bubbles v0.13.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
-github.com/charmbracelet/bubbletea v0.21.0 h1:f3y+kanzgev5PA916qxmDybSHU3N804uOnKnhRPXTcI=
-github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
-github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
-github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
-github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
-github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
-github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
-github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/evertras/bubble-table v0.15.2 h1:hVj27V9tk5TD5p6mVv0RK/KJu2sHq0U+mBMux/HptkU=
-github.com/evertras/bubble-table v0.15.2/go.mod h1:SPOZKbIpyYWPHBNki3fyNpiPBQkvkULAtOT7NTD5fKY=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
-github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
+github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
-github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
-github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
-github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
-github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
-github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
-github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
-github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
-github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
-github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
-github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 h1:Yl0tPBa8QPjGmesFh1D0rDy+q1Twx6FyU7VWHi8wZbI=
-github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852/go.mod h1:eqOVx5Vwu4gd2mmMZvVZsgIqNSaW3xxRThUJ0k/TPk4=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
-github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
-github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
+github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
-github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
-github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
+google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@@ -132,3 +218,4 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/main.go b/main.go
index db2632c..c591672 100644
--- a/main.go
+++ b/main.go
@@ -22,7 +22,6 @@ import (
"flag"
"fmt"
"io"
- "io/ioutil"
"net/http"
"os"
@@ -83,7 +82,12 @@ func main() {
if config.LogLevel == "debug" {
log.Debug("Starting profiler on 127.0.0.1:6080")
- go http.ListenAndServe("127.0.0.1:6080", nil)
+ go func() {
+ err := http.ListenAndServe("127.0.0.1:6080", nil)
+ if err != nil {
+
+ }
+ }()
}
log.Debugf("loaded configuration: %+v", config)
@@ -112,9 +116,9 @@ func configReader() io.Reader {
log.Fatalln("unable to open config file", err)
}
- c, err := ioutil.ReadAll(configFile)
- if err != nil {
- log.Fatalln("unable to read config file", err)
+ c, rErr := io.ReadAll(configFile)
+ if rErr != nil {
+ log.Fatalln("unable to read config file", rErr)
}
return bytes.NewReader(c)
diff --git a/resourcegroups/resourcegroups.go b/resourcegroups/resourcegroups.go
new file mode 100644
index 0000000..f10556a
--- /dev/null
+++ b/resourcegroups/resourcegroups.go
@@ -0,0 +1,165 @@
+package resourcegroups
+
+import (
+ "fmt"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/resourcegroups"
+ "github.com/aws/aws-sdk-go/service/resourcegroups/resourcegroupsiface"
+ log "github.com/sirupsen/logrus"
+)
+
+// ResourceGroups is a wrapper around the aws Resource Groups service
+type ResourceGroups struct {
+ session *session.Session
+ Service resourcegroupsiface.ResourceGroupsAPI
+}
+
+// CreateGroupInput represents the input parameters for creating a resource group
+type CreateGroupInput struct {
+ Name string
+ Description string
+ ResourceQuery *resourcegroups.ResourceQuery
+}
+
+// Option A resource group option
+type Option func(*ResourceGroups)
+
+// New creates a new ResourceGroups
+func New(opts ...Option) *ResourceGroups {
+ rg := ResourceGroups{}
+
+ for _, opt := range opts {
+ opt(&rg)
+ }
+
+ if rg.session != nil {
+ rg.Service = resourcegroups.New(rg.session)
+ }
+
+ return &rg
+}
+
+// WithSession make sure the connection is using aws session functionality
+func WithSession(sess *session.Session) Option {
+ return func(rg *ResourceGroups) {
+ log.Debug("using aws session")
+ rg.session = sess
+ }
+}
+
+// WithCredentials configures the resources group aws client with the given credentials
+func WithCredentials(key, secret, token, region string) Option {
+ return func(rg *ResourceGroups) {
+ log.Debugf("creating new session with key id %s in region %s", key, region)
+ sess := session.Must(session.NewSession(&aws.Config{
+ Credentials: credentials.NewStaticCredentials(key, secret, token),
+ Region: aws.String(region),
+ }))
+ rg.session = sess
+ }
+}
+
+// CreateGroup creates a new AWS resource group with the specified configuration
+func (rg *ResourceGroups) CreateGroup(input CreateGroupInput) (*resourcegroups.Group, error) {
+ if rg.Service == nil {
+ return nil, fmt.Errorf("resource groups service not initialized")
+ }
+
+ params := &resourcegroups.CreateGroupInput{
+ Name: aws.String(input.Name),
+ ResourceQuery: input.ResourceQuery,
+ }
+
+ if input.Description != "" {
+ params.Description = aws.String(input.Description)
+ }
+
+ result, err := rg.Service.CreateGroup(params)
+ if err != nil {
+ return nil, err
+ }
+
+ return result.Group, nil
+}
+
+// ListGroups returns all resource groups in the account
+func (rg *ResourceGroups) ListGroups() ([]*resourcegroups.Group, error) {
+ if rg.Service == nil {
+ return nil, fmt.Errorf("resource groups service not initialized")
+ }
+
+ input := &resourcegroups.ListGroupsInput{}
+ var groups []*resourcegroups.Group
+
+ // Handle pagination
+ err := rg.Service.ListGroupsPages(input, func(page *resourcegroups.ListGroupsOutput, lastPage bool) bool {
+ groups = append(groups, page.Groups...)
+ return !lastPage
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ return groups, nil
+}
+
+// GetGroup retrieves details of a specific resource group
+func (rg *ResourceGroups) GetGroup(name string) (*resourcegroups.Group, error) {
+ if rg.Service == nil {
+ return nil, fmt.Errorf("resource groups service not initialized")
+ }
+
+ input := &resourcegroups.GetGroupInput{
+ GroupName: aws.String(name),
+ }
+
+ result, err := rg.Service.GetGroup(input)
+ if err != nil {
+ return nil, err
+ }
+
+ return result.Group, nil
+}
+
+// ListGroupResources lists all resources in a specific group
+func (rg *ResourceGroups) ListGroupResources(name string) ([]*resourcegroups.ListGroupResourcesItem, error) {
+ if rg.Service == nil {
+ return nil, fmt.Errorf("resource groups service not initialized")
+ }
+
+ input := &resourcegroups.ListGroupResourcesInput{
+ GroupName: aws.String(name),
+ }
+
+ var resources []*resourcegroups.ListGroupResourcesItem
+
+ // Handle pagination
+ err := rg.Service.ListGroupResourcesPages(input, func(page *resourcegroups.ListGroupResourcesOutput, lastPage bool) bool {
+ resources = append(resources, page.Resources...)
+ return !lastPage
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ return resources, nil
+}
+
+// DeleteGroup deletes a resource group
+func (rg *ResourceGroups) DeleteGroup(name string) error {
+ if rg.Service == nil {
+ return fmt.Errorf("resource groups service not initialized")
+ }
+
+ input := &resourcegroups.DeleteGroupInput{
+ GroupName: aws.String(name),
+ }
+
+ _, err := rg.Service.DeleteGroup(input)
+ return err
+}
diff --git a/resourcegroups/resourcegroups_test.go b/resourcegroups/resourcegroups_test.go
new file mode 100644
index 0000000..56537c3
--- /dev/null
+++ b/resourcegroups/resourcegroups_test.go
@@ -0,0 +1,712 @@
+package resourcegroups
+
+import (
+ "fmt"
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/service/resourcegroups"
+ "github.com/aws/aws-sdk-go/service/resourcegroups/resourcegroupsiface"
+ "testing"
+
+ "github.com/aws/aws-sdk-go/aws/session"
+)
+
+// MockResourceGroupsClient is a mock implementation of resourcegroupsiface.ResourceGroupsAPI
+type MockResourceGroupsClient struct {
+ resourcegroupsiface.ResourceGroupsAPI
+ mockCreateGroup func(*resourcegroups.CreateGroupInput) (*resourcegroups.CreateGroupOutput, error)
+ mockListGroupsPages func(*resourcegroups.ListGroupsInput, func(*resourcegroups.ListGroupsOutput, bool) bool) error
+ mockGetGroup func(*resourcegroups.GetGroupInput) (*resourcegroups.GetGroupOutput, error)
+ mockListGroupResourcesPages func(*resourcegroups.ListGroupResourcesInput, func(*resourcegroups.ListGroupResourcesOutput, bool) bool) error
+ mockDeleteGroup func(*resourcegroups.DeleteGroupInput) (*resourcegroups.DeleteGroupOutput, error)
+}
+
+func (m *MockResourceGroupsClient) CreateGroup(input *resourcegroups.CreateGroupInput) (*resourcegroups.CreateGroupOutput, error) {
+ if m.mockCreateGroup != nil {
+ return m.mockCreateGroup(input)
+ }
+ return nil, nil
+}
+
+func (m *MockResourceGroupsClient) ListGroupsPages(input *resourcegroups.ListGroupsInput, fn func(*resourcegroups.ListGroupsOutput, bool) bool) error {
+ if m.mockListGroupsPages != nil {
+ return m.mockListGroupsPages(input, fn)
+ }
+ return nil
+}
+
+func (m *MockResourceGroupsClient) GetGroup(input *resourcegroups.GetGroupInput) (*resourcegroups.GetGroupOutput, error) {
+ if m.mockGetGroup != nil {
+ return m.mockGetGroup(input)
+ }
+ return nil, nil
+}
+
+func (m *MockResourceGroupsClient) ListGroupResourcesPages(input *resourcegroups.ListGroupResourcesInput, fn func(*resourcegroups.ListGroupResourcesOutput, bool) bool) error {
+ if m.mockListGroupResourcesPages != nil {
+ return m.mockListGroupResourcesPages(input, fn)
+ }
+ return nil
+}
+
+func (m *MockResourceGroupsClient) DeleteGroup(input *resourcegroups.DeleteGroupInput) (*resourcegroups.DeleteGroupOutput, error) {
+ if m.mockDeleteGroup != nil {
+ return m.mockDeleteGroup(input)
+ }
+ return nil, nil
+}
+
+func TestWithSession(t *testing.T) {
+ sess := session.Must(session.NewSession())
+ rg := New(WithSession(sess))
+
+ if rg.session != sess {
+ t.Error("WithSession option did not set the session correctly")
+ }
+
+ if rg.Service == nil {
+ t.Error("Service was not initialized with session")
+ }
+}
+
+func TestWithCredentials(t *testing.T) {
+ cases := []struct {
+ name string
+ key string
+ secret string
+ token string
+ region string
+ }{
+ {
+ name: "with all credentials",
+ key: "test-key",
+ secret: "test-secret",
+ token: "test-token",
+ region: "us-east-1",
+ },
+ {
+ name: "without token",
+ key: "test-key",
+ secret: "test-secret",
+ token: "",
+ region: "us-west-2",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ rg := New(WithCredentials(tc.key, tc.secret, tc.token, tc.region))
+
+ if rg.session == nil {
+ t.Error("session was not created")
+ }
+
+ if rg.Service == nil {
+ t.Error("Service was not initialized")
+ }
+ })
+ }
+}
+
+func TestNewWithMultipleOptions(t *testing.T) {
+ cases := []struct {
+ name string
+ opts []Option
+ wantNil bool
+ }{
+ {
+ name: "no options",
+ opts: []Option{},
+ wantNil: true,
+ },
+ {
+ name: "with credentials",
+ opts: []Option{
+ WithCredentials("key", "secret", "", "us-east-1"),
+ },
+ wantNil: false,
+ },
+ {
+ name: "with session",
+ opts: []Option{
+ WithSession(session.Must(session.NewSession())),
+ },
+ wantNil: false,
+ },
+ {
+ name: "with both options",
+ opts: []Option{
+ WithSession(session.Must(session.NewSession())),
+ WithCredentials("key", "secret", "", "us-east-1"),
+ },
+ wantNil: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ rg := New(tc.opts...)
+
+ if tc.wantNil {
+ if rg.Service != nil {
+ t.Error("Service should be nil when no options are provided")
+ }
+ } else {
+ if rg.Service == nil {
+ t.Error("Service should not be nil when options are provided")
+ }
+ }
+ })
+ }
+}
+
+func TestCreateGroup(t *testing.T) {
+ cases := []struct {
+ name string
+ input CreateGroupInput
+ mockResponse *resourcegroups.CreateGroupOutput
+ mockErr error
+ expectedError bool
+ }{
+ {
+ name: "successful creation",
+ input: CreateGroupInput{
+ Name: "test-group",
+ Description: "test description",
+ ResourceQuery: &resourcegroups.ResourceQuery{
+ Type: aws.String("TAG_FILTERS_1_0"),
+ Query: aws.String(`{
+ "ResourceTypeFilters": ["AWS::EC2::Instance"],
+ "TagFilters": [
+ {
+ "Key": "Environment",
+ "Values": ["Test"]
+ }
+ ]
+ }`),
+ },
+ },
+ mockResponse: &resourcegroups.CreateGroupOutput{
+ Group: &resourcegroups.Group{
+ GroupArn: aws.String("arn:aws:resource-groups:us-east-1:123456789012:group/test-group"),
+ Name: aws.String("test-group"),
+ Description: aws.String("test description"),
+ },
+ },
+ expectedError: false,
+ },
+ {
+ name: "aws error",
+ input: CreateGroupInput{
+ Name: "test-group",
+ ResourceQuery: &resourcegroups.ResourceQuery{
+ Type: aws.String("TAG_FILTERS_1_0"),
+ Query: aws.String("{}"),
+ },
+ },
+ mockErr: fmt.Errorf("AWS error"),
+ expectedError: true,
+ },
+ {
+ name: "nil service",
+ input: CreateGroupInput{
+ Name: "test-group",
+ ResourceQuery: &resourcegroups.ResourceQuery{
+ Type: aws.String("TAG_FILTERS_1_0"),
+ Query: aws.String("{}"),
+ },
+ },
+ expectedError: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := &MockResourceGroupsClient{
+ mockCreateGroup: func(input *resourcegroups.CreateGroupInput) (*resourcegroups.CreateGroupOutput, error) {
+ if tc.mockErr != nil {
+ return nil, tc.mockErr
+ }
+ return tc.mockResponse, nil
+ },
+ }
+
+ rg := &ResourceGroups{
+ Service: mock,
+ }
+
+ if tc.name == "nil service" {
+ rg.Service = nil
+ }
+
+ group, err := rg.CreateGroup(tc.input)
+
+ if tc.expectedError {
+ if err == nil {
+ t.Error("expected error but got nil")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if group == nil {
+ t.Error("expected group but got nil")
+ }
+ }
+ })
+ }
+}
+
+func TestListGroups(t *testing.T) {
+ cases := []struct {
+ name string
+ mockResponse []*resourcegroups.Group
+ mockErr error
+ expectedError bool
+ nilService bool
+ }{
+ {
+ name: "successful listing - single page",
+ mockResponse: []*resourcegroups.Group{
+ {
+ GroupArn: aws.String("arn:aws:resource-groups:us-east-1:123456789012:group/test-group-1"),
+ Name: aws.String("test-group-1"),
+ Description: aws.String("test description 1"),
+ },
+ {
+ GroupArn: aws.String("arn:aws:resource-groups:us-east-1:123456789012:group/test-group-2"),
+ Name: aws.String("test-group-2"),
+ Description: aws.String("test description 2"),
+ },
+ },
+ expectedError: false,
+ },
+ {
+ name: "successful listing - multiple pages",
+ mockResponse: []*resourcegroups.Group{
+ {
+ GroupArn: aws.String("arn:aws:resource-groups:us-east-1:123456789012:group/test-group-1"),
+ Name: aws.String("test-group-1"),
+ },
+ {
+ GroupArn: aws.String("arn:aws:resource-groups:us-east-1:123456789012:group/test-group-2"),
+ Name: aws.String("test-group-2"),
+ },
+ {
+ GroupArn: aws.String("arn:aws:resource-groups:us-east-1:123456789012:group/test-group-3"),
+ Name: aws.String("test-group-3"),
+ },
+ },
+ expectedError: false,
+ },
+ {
+ name: "aws error",
+ mockErr: fmt.Errorf("AWS error"),
+ expectedError: true,
+ },
+ {
+ name: "nil service",
+ nilService: true,
+ expectedError: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := &MockResourceGroupsClient{
+ mockListGroupsPages: func(input *resourcegroups.ListGroupsInput, fn func(*resourcegroups.ListGroupsOutput, bool) bool) error {
+ if tc.mockErr != nil {
+ return tc.mockErr
+ }
+
+ if len(tc.mockResponse) > 2 {
+ // Simulate pagination for responses with more than 2 items
+ fn(&resourcegroups.ListGroupsOutput{
+ Groups: tc.mockResponse[:2],
+ }, false)
+ fn(&resourcegroups.ListGroupsOutput{
+ Groups: tc.mockResponse[2:],
+ }, true)
+ } else {
+ fn(&resourcegroups.ListGroupsOutput{
+ Groups: tc.mockResponse,
+ }, true)
+ }
+ return nil
+ },
+ }
+
+ rg := &ResourceGroups{
+ Service: mock,
+ }
+
+ if tc.nilService {
+ rg.Service = nil
+ }
+
+ groups, err := rg.ListGroups()
+
+ if tc.expectedError {
+ if err == nil {
+ t.Error("expected error but got nil")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if groups == nil {
+ t.Error("expected groups but got nil")
+ }
+ if len(groups) != len(tc.mockResponse) {
+ t.Errorf("expected %d groups but got %d", len(tc.mockResponse), len(groups))
+ }
+ // Verify each group matches the mock response
+ for i, group := range groups {
+ if *group.GroupArn != *tc.mockResponse[i].GroupArn {
+ t.Errorf("expected group ARN %s but got %s", *tc.mockResponse[i].GroupArn, *group.GroupArn)
+ }
+ if *group.Name != *tc.mockResponse[i].Name {
+ t.Errorf("expected group name %s but got %s", *tc.mockResponse[i].Name, *group.Name)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestGetGroup(t *testing.T) {
+ cases := []struct {
+ name string
+ groupName string
+ mockResponse *resourcegroups.GetGroupOutput
+ mockErr error
+ expectedError bool
+ nilService bool
+ }{
+ {
+ name: "successful retrieval",
+ groupName: "test-group",
+ mockResponse: &resourcegroups.GetGroupOutput{
+ Group: &resourcegroups.Group{
+ GroupArn: aws.String("arn:aws:resource-groups:us-east-1:123456789012:group/test-group"),
+ Name: aws.String("test-group"),
+ Description: aws.String("test description"),
+ },
+ },
+ expectedError: false,
+ },
+ {
+ name: "group not found",
+ groupName: "non-existent-group",
+ mockErr: fmt.Errorf("ResourceNotFoundException: Group not found"),
+ expectedError: true,
+ },
+ {
+ name: "aws error",
+ groupName: "test-group",
+ mockErr: fmt.Errorf("AWS error"),
+ expectedError: true,
+ },
+ {
+ name: "nil service",
+ groupName: "test-group",
+ nilService: true,
+ expectedError: true,
+ },
+ {
+ name: "empty group name",
+ groupName: "",
+ mockErr: fmt.Errorf("ValidationException: Group name is required"),
+ expectedError: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := &MockResourceGroupsClient{
+ mockGetGroup: func(input *resourcegroups.GetGroupInput) (*resourcegroups.GetGroupOutput, error) {
+ // Verify input
+ if input.GroupName == nil {
+ t.Error("GroupName should not be nil")
+ } else if *input.GroupName != tc.groupName {
+ t.Errorf("expected group name %s but got %s", tc.groupName, *input.GroupName)
+ }
+
+ if tc.mockErr != nil {
+ return nil, tc.mockErr
+ }
+ return tc.mockResponse, nil
+ },
+ }
+
+ rg := &ResourceGroups{
+ Service: mock,
+ }
+
+ if tc.nilService {
+ rg.Service = nil
+ }
+
+ group, err := rg.GetGroup(tc.groupName)
+
+ if tc.expectedError {
+ if err == nil {
+ t.Error("expected error but got nil")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if group == nil {
+ t.Error("expected group but got nil")
+ } else {
+ // Verify group details
+ expectedGroup := tc.mockResponse.Group
+ if *group.GroupArn != *expectedGroup.GroupArn {
+ t.Errorf("expected group ARN %s but got %s", *expectedGroup.GroupArn, *group.GroupArn)
+ }
+ if *group.Name != *expectedGroup.Name {
+ t.Errorf("expected group name %s but got %s", *expectedGroup.Name, *group.Name)
+ }
+ if *group.Description != *expectedGroup.Description {
+ t.Errorf("expected group description %s but got %s", *expectedGroup.Description, *group.Description)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestListGroupResources(t *testing.T) {
+ cases := []struct {
+ name string
+ groupName string
+ mockResponse []*resourcegroups.ListGroupResourcesItem
+ mockErr error
+ expectedError bool
+ nilService bool
+ }{
+ {
+ name: "successful listing - single page",
+ groupName: "test-group",
+ mockResponse: []*resourcegroups.ListGroupResourcesItem{
+ {
+ Identifier: &resourcegroups.ResourceIdentifier{
+ ResourceArn: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0"),
+ ResourceType: aws.String("AWS::EC2::Instance"),
+ },
+ },
+ {
+ Identifier: &resourcegroups.ResourceIdentifier{
+ ResourceArn: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-0987654321fedcba0"),
+ ResourceType: aws.String("AWS::EC2::Instance"),
+ },
+ },
+ },
+ expectedError: false,
+ },
+ {
+ name: "successful listing - multiple pages",
+ groupName: "test-group",
+ mockResponse: []*resourcegroups.ListGroupResourcesItem{
+ {
+ Identifier: &resourcegroups.ResourceIdentifier{
+ ResourceArn: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-1"),
+ ResourceType: aws.String("AWS::EC2::Instance"),
+ },
+ },
+ {
+ Identifier: &resourcegroups.ResourceIdentifier{
+ ResourceArn: aws.String("arn:aws:ec2:us-east-1:123456789012:instance/i-2"),
+ ResourceType: aws.String("AWS::EC2::Instance"),
+ },
+ },
+ {
+ Identifier: &resourcegroups.ResourceIdentifier{
+ ResourceArn: aws.String("arn:aws:s3:us-east-1:123456789012:bucket/test-bucket"),
+ ResourceType: aws.String("AWS::S3::Bucket"),
+ },
+ },
+ },
+ expectedError: false,
+ },
+ {
+ name: "group not found",
+ groupName: "non-existent-group",
+ mockErr: fmt.Errorf("ResourceNotFoundException: Group not found"),
+ expectedError: true,
+ },
+ {
+ name: "aws error",
+ groupName: "test-group",
+ mockErr: fmt.Errorf("AWS error"),
+ expectedError: true,
+ },
+ {
+ name: "nil service",
+ groupName: "test-group",
+ nilService: true,
+ expectedError: true,
+ },
+ {
+ name: "empty group name",
+ groupName: "",
+ mockErr: fmt.Errorf("ValidationException: Group name is required"),
+ expectedError: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := &MockResourceGroupsClient{
+ mockListGroupResourcesPages: func(input *resourcegroups.ListGroupResourcesInput, fn func(*resourcegroups.ListGroupResourcesOutput, bool) bool) error {
+ // Verify input
+ if input.GroupName == nil {
+ t.Error("GroupName should not be nil")
+ } else if *input.GroupName != tc.groupName {
+ t.Errorf("expected group name %s but got %s", tc.groupName, *input.GroupName)
+ }
+
+ if tc.mockErr != nil {
+ return tc.mockErr
+ }
+
+ if len(tc.mockResponse) > 2 {
+ // Simulate pagination for responses with more than 2 items
+ fn(&resourcegroups.ListGroupResourcesOutput{
+ Resources: tc.mockResponse[:2],
+ }, false)
+ fn(&resourcegroups.ListGroupResourcesOutput{
+ Resources: tc.mockResponse[2:],
+ }, true)
+ } else {
+ fn(&resourcegroups.ListGroupResourcesOutput{
+ Resources: tc.mockResponse,
+ }, true)
+ }
+ return nil
+ },
+ }
+
+ rg := &ResourceGroups{
+ Service: mock,
+ }
+
+ if tc.nilService {
+ rg.Service = nil
+ }
+
+ resources, err := rg.ListGroupResources(tc.groupName)
+
+ if tc.expectedError {
+ if err == nil {
+ t.Error("expected error but got nil")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ if resources == nil {
+ t.Error("expected resources but got nil")
+ }
+ if len(resources) != len(tc.mockResponse) {
+ t.Errorf("expected %d resources but got %d", len(tc.mockResponse), len(resources))
+ }
+ // Verify each resource matches the mock response
+ for i, resource := range resources {
+ if *resource.Identifier.ResourceArn != *tc.mockResponse[i].Identifier.ResourceArn {
+ t.Errorf("expected resource ARN %s but got %s",
+ *tc.mockResponse[i].Identifier.ResourceArn,
+ *resource.Identifier.ResourceArn)
+ }
+ if *resource.Identifier.ResourceType != *tc.mockResponse[i].Identifier.ResourceType {
+ t.Errorf("expected resource type %s but got %s",
+ *tc.mockResponse[i].Identifier.ResourceType,
+ *resource.Identifier.ResourceType)
+ }
+ }
+ }
+ })
+ }
+}
+
+func TestDeleteGroup(t *testing.T) {
+ cases := []struct {
+ name string
+ groupName string
+ mockResponse *resourcegroups.DeleteGroupOutput
+ mockErr error
+ expectedError bool
+ nilService bool
+ }{
+ {
+ name: "successful deletion",
+ groupName: "test-group",
+ mockResponse: &resourcegroups.DeleteGroupOutput{},
+ expectedError: false,
+ },
+ {
+ name: "group not found",
+ groupName: "non-existent-group",
+ mockErr: fmt.Errorf("ResourceNotFoundException: Group not found"),
+ expectedError: true,
+ },
+ {
+ name: "aws error",
+ groupName: "test-group",
+ mockErr: fmt.Errorf("AWS error"),
+ expectedError: true,
+ },
+ {
+ name: "nil service",
+ groupName: "test-group",
+ nilService: true,
+ expectedError: true,
+ },
+ {
+ name: "empty group name",
+ groupName: "",
+ mockErr: fmt.Errorf("ValidationException: Group name is required"),
+ expectedError: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := &MockResourceGroupsClient{
+ mockDeleteGroup: func(input *resourcegroups.DeleteGroupInput) (*resourcegroups.DeleteGroupOutput, error) {
+ // Verify input
+ if input.GroupName == nil {
+ t.Error("GroupName should not be nil")
+ } else if *input.GroupName != tc.groupName {
+ t.Errorf("expected group name %s but got %s", tc.groupName, *input.GroupName)
+ }
+
+ if tc.mockErr != nil {
+ return nil, tc.mockErr
+ }
+ return tc.mockResponse, nil
+ },
+ }
+
+ rg := &ResourceGroups{
+ Service: mock,
+ }
+
+ if tc.nilService {
+ rg.Service = nil
+ }
+
+ err := rg.DeleteGroup(tc.groupName)
+
+ if tc.expectedError {
+ if err == nil {
+ t.Error("expected error but got nil")
+ }
+ } else {
+ if err != nil {
+ t.Errorf("unexpected error: %v", err)
+ }
+ }
+ })
+ }
+}