initial revision
This commit is contained in:
134
internal/cmd/hibernate.go
Normal file
134
internal/cmd/hibernate.go
Normal 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
157
internal/cmd/secrets.go
Normal 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
139
internal/cmd/update-a.go
Normal 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
21
internal/cmd/util.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user