initial revision

This commit is contained in:
2024-12-12 20:22:11 -05:00
commit 3fbd407254
16 changed files with 1190 additions and 0 deletions

134
internal/cmd/hibernate.go Normal file
View File

@@ -0,0 +1,134 @@
package cmd
import (
"fmt"
"time"
cli "github.com/urfave/cli/v2"
"aws-mgmt/pkg/client"
)
const hibernateDesc = `Hibernate an EC2 instance.
The TIME string is the number of seconds to delay the hibernation, and defaults
to 10. "now" is an alias for 0.
If run on an EC2 instance with an appropriate IAM EC2 role, the instance will
be hibernated. Required IAM permissions include:
- ec2:StopInstances
Alternatively, an instance ID must be specified: that instance will be
hibernated. Required IAM permissions include:
- ec2:StopInstances
`
var HibernateApp = &cli.App{
Name: "hibernate",
Usage: "hibernate an instance",
UsageText: "hibernate [options] [TIME]",
HideHelpCommand: true,
Args: true,
ArgsUsage: "[TIME]",
Action: hibernateFunc,
Description: hibernateDesc,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance-id",
Value: "",
Usage: "set the instance id, defaults to the instance running this command",
},
&cli.StringFlag{
Name: "loglevel",
Value: "error",
Usage: "set the loglevel: debug, info, error",
},
},
}
var HibernateCommand = &cli.Command{
Name: "hibernate",
Usage: "hibernate an instance",
UsageText: "hibernate [options] [TIME]",
HideHelpCommand: true,
Args: true,
ArgsUsage: "[TIME]",
Action: hibernateFunc,
Description: hibernateDesc,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance-id",
Value: "",
Usage: "set the instance id, defaults to the instance running this command",
},
&cli.StringFlag{
Name: "loglevel",
Value: "error",
Usage: "set the loglevel: debug, info, error",
},
},
}
func hibernateFunc(ctx *cli.Context) error {
// create logger
log, err := createLogger(ctx.String("loglevel"))
if err != nil {
return fmt.Errorf("unable to create a logger, %w", err)
}
// set timer for shutdown
timer, err := setTimer(ctx.Args().Get(0))
if err != nil {
return err
}
log.Debug("timer is set", "timer", timer)
// create aws config
cfg, err := client.CreateAWSConfig()
if err != nil {
return err
}
awsClient := client.NewAWSClient(
client.SetConfig(cfg),
)
// determine instance id
instance_id := ctx.String("instance-id")
if instance_id == "" {
instance_id, err = awsClient.GetInstanceID()
if err != nil {
return err
}
}
log.Debug("instance id is set", "instance_id", instance_id)
log.Info("waiting to hibernate", "timer", timer)
time.Sleep(timer)
// stop instance
state, err := awsClient.StopInstance(instance_id, true)
if err != nil {
return fmt.Errorf("unable to stop instance, %w", err)
}
log.Info("hibernating instance", "instance_id", instance_id, "state", state)
return nil
}
func setTimer(t string) (time.Duration, error) {
var err error
timer := time.Duration(10 * time.Second)
if t != "" {
if t == "now" {
timer = time.Duration(0)
} else {
timer, err = time.ParseDuration(t + "s")
if err != nil {
return timer, fmt.Errorf("unable to set timer, %w", err)
}
}
}
return timer, nil
}

157
internal/cmd/secrets.go Normal file
View File

@@ -0,0 +1,157 @@
package cmd
import (
"fmt"
"os"
"strings"
"time"
"github.com/charmbracelet/huh"
"github.com/pquerna/otp/totp"
cli "github.com/urfave/cli/v2"
"aws-mgmt/pkg/client"
)
const secretsDesc = `View secrets from AWS Secrets Manager.
`
var SecretsApp = &cli.App{
Name: "secrets",
Usage: "view secrets",
UsageText: "secrets [options]",
HideHelpCommand: true,
Args: true,
ArgsUsage: "[secret]",
Action: secretsFunc,
Description: secretsDesc,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "totp",
Usage: "Generate a MFA token based on the secret value",
},
&cli.StringSliceFlag{
Name: "tags",
Usage: "Tags in the format key=value",
},
&cli.BoolFlag{
Name: "sh",
Usage: "Print the equivalent AWS CLI / shell command(s)",
},
},
}
var SecretsCommand = &cli.Command{
Name: "secrets",
Usage: "view secrets",
UsageText: "secrets [options]",
HideHelpCommand: true,
Args: true,
ArgsUsage: "[secret]",
Action: secretsFunc,
Description: secretsDesc,
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "totp",
Usage: "Return a MFA token",
},
&cli.StringSliceFlag{
Name: "tags",
Usage: "Tags in the format key=value",
},
},
}
func secretsFunc(c *cli.Context) error {
// create aws config
cfg, err := client.CreateAWSConfig()
if err != nil {
return err
}
awsClient := client.NewAWSClient(
client.SetConfig(cfg),
)
// get hostname & domain
secret := c.Args().Get(0)
tags := parseTags(c.StringSlice("tags"))
if secret == "" {
secrets, err := awsClient.ListSecrets(tags)
if err != nil {
return err
}
if c.Bool("sh") {
str := `aws secretsmanager list-secrets --query 'SecretList[].[Name,Description]' --output text | sort`
fmt.Fprintln(os.Stderr, str)
}
if len(secrets) == 0 {
return fmt.Errorf("no secrets found")
}
secret, err = selectSecretDialog(secrets)
if err != nil {
return err
}
}
// get secret value for secretName
secretValue, err := awsClient.GetSecretValue(secret)
if err != nil {
return err
}
// if the totp flag is set, generate a token
if c.Bool("totp") {
token, err := totp.GenerateCode(secretValue, time.Now())
if err != nil {
return err
}
fmt.Println(token)
} else {
// print the aws cli equivalent command to output this secret to stderr
if c.Bool("sh") {
fmt.Fprintln(os.Stderr, fmt.Sprintf("aws secretsmanager get-secret-value --secret-id %s --query SecretString --output text --no-cli-pager",secret))
}
fmt.Println(secretValue)
}
return nil
}
func parseTags(rawTags []string) map[string]string {
tags := make(map[string]string)
for _, tag := range rawTags {
parts := strings.SplitN(tag, "=", 2)
if len(parts) == 2 {
tags[parts[0]] = parts[1]
}
}
return tags
}
// selectSecretDialog prompts the user to select a secret from a list of SecretListEntry
func selectSecretDialog(secretList []client.SecretListEntry) (string, error) {
// create a select prompt for the user to choose a secret
var secretName string
options := make([]huh.Option[string], len(secretList))
// options := make([]huh.Option, len(secretList))
for i, secret := range secretList {
// create a new option for each secret
options[i] = huh.NewOption[string](strings.TrimSuffix(fmt.Sprintf("%s: %s", secret.Name, secret.Description), ": "), secret.Name)
}
huh.NewSelect[string]().
Title("Select a secret").
Options(options...).
Value(&secretName).
Run()
return secretName, nil
}

139
internal/cmd/update-a.go Normal file
View File

@@ -0,0 +1,139 @@
package cmd
import (
"fmt"
"strings"
cli "github.com/urfave/cli/v2"
"aws-mgmt/pkg/client"
)
const updateADesc = `Update a DNS A record for an instance.
The only required argument is the FQDN: other information required to update
the appropriate Route53 record is determined by querying Route53 and IMDS (or
EC2) APIs.
If run on an EC2 instance with an appropriate IAM EC2 role, an A record will be
created for that instance. Required IAM permissions include:
- route53:ListHostedZonesByName
- route53:ChangeResourceRecordSets
Alternatively, an instance ID must be specified: an A record will be created
for that instance. Required IAM permissions include:
- route53:ListHostedZonesByName
- route53:ChangeResourceRecordSets
- ec2:DescribeInstances`
var UpdateAApp = &cli.App{
Name: "update-a",
Usage: "Create / update an A record for an instance with a public IP",
UsageText: "update-a [options] <FQDN>",
HideHelpCommand: true,
Args: true,
ArgsUsage: "<FQDN>",
Action: updateAFunc,
Description: updateADesc,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance-id",
Value: "",
Usage: "set the instance id, defaults to the instance running this command",
},
&cli.StringFlag{
Name: "loglevel",
Value: "error",
Usage: "set the loglevel: debug, info, error",
},
},
}
var UpdateACommand = &cli.Command{
Name: "update-a",
Usage: "Create / update an A record for an instance with a public IP",
UsageText: "update-a [options] <FQDN>",
HideHelpCommand: true,
Args: true,
ArgsUsage: "<FQDN>",
Action: updateAFunc,
Description: updateADesc,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "instance-id",
Value: "",
Usage: "set the instance id, defaults to the instance running this command",
},
&cli.StringFlag{
Name: "loglevel",
Value: "error",
Usage: "set the loglevel: debug, info, error",
},
},
}
func updateAFunc(ctx *cli.Context) error {
// create logger
log, err := createLogger(ctx.String("loglevel"))
if err != nil {
return fmt.Errorf("unable to create a logger, %w", err)
}
// create aws config
cfg, err := client.CreateAWSConfig()
if err != nil {
return err
}
awsClient := client.NewAWSClient(
client.SetConfig(cfg),
)
// determine instance id
instance_id := ctx.String("instance-id")
// get hostname & domain
hostname := ctx.Args().Get(0)
domain, err := parseHostname(hostname)
if err != nil {
return err
}
log.Debug("input received", "instance-id", instance_id, "hostname", hostname, "domain", domain)
zoneID, err := awsClient.GetZoneID(domain)
if err != nil {
return err
}
log.Debug("zone id set", "zone-id", zoneID)
var publicIP string
if instance_id != "" {
log.Debug("attempting to retrieve public ip from the EC2 API")
publicIP, err = awsClient.GetEC2PublicIP(instance_id)
} else {
log.Debug("attempting to retrieve public ip from IMDS")
publicIP, err = awsClient.GetMetadata("public-ipv4")
}
if err != nil {
return err
}
log.Debug("public ip retrieved", "public_ip", publicIP)
err = awsClient.UpdateA(hostname, publicIP, zoneID)
if err != nil {
return err
}
log.Info("dns record updated", "hostname", hostname, "zone-id", zoneID, "public-ipv4", publicIP)
return nil
}
func parseHostname(h string) (string, error) {
var domain string
if idx := strings.IndexByte(h, '.'); idx >= 0 {
domain = h[idx+1:]
} else {
return "", fmt.Errorf("hostname (%s) must be a FQDN", h)
}
return domain, nil
}

21
internal/cmd/util.go Normal file
View File

@@ -0,0 +1,21 @@
package cmd
import (
"fmt"
"log/slog"
"os"
)
/* create a logger at the specified loglevel */
func createLogger(lvl string) (*slog.Logger, error) {
level := slog.LevelError
err := level.UnmarshalText([]byte(lvl))
if err != nil {
return nil, fmt.Errorf("invalid log level, %w", err)
}
log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: level,
}))
return log, nil
}