From 3fbd4072541ab3a7e42e18a8ed031bb41ffc8d29 Mon Sep 17 00:00:00 2001 From: Jason Swank Date: Thu, 12 Dec 2024 20:22:11 -0500 Subject: [PATCH] initial revision --- .gitignore | 1 + Taskfile.yml | 79 ++++++++++++ cmd/aws-mgmt/root.go | 32 +++++ cmd/hibernate/root.go | 19 +++ cmd/secrets/root.go | 19 +++ cmd/update-a/root.go | 18 +++ dist/config.yaml | 115 +++++++++++++++++ dist/metadata.json | 1 + go.mod | 59 +++++++++ go.sum | 116 +++++++++++++++++ internal/cmd/hibernate.go | 134 +++++++++++++++++++ internal/cmd/secrets.go | 157 ++++++++++++++++++++++ internal/cmd/update-a.go | 139 ++++++++++++++++++++ internal/cmd/util.go | 21 +++ pkg/client/aws.go | 265 ++++++++++++++++++++++++++++++++++++++ pkg/logout/now.go | 15 +++ 16 files changed, 1190 insertions(+) create mode 100644 .gitignore create mode 100644 Taskfile.yml create mode 100644 cmd/aws-mgmt/root.go create mode 100644 cmd/hibernate/root.go create mode 100644 cmd/secrets/root.go create mode 100644 cmd/update-a/root.go create mode 100644 dist/config.yaml create mode 100644 dist/metadata.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cmd/hibernate.go create mode 100644 internal/cmd/secrets.go create mode 100644 internal/cmd/update-a.go create mode 100644 internal/cmd/util.go create mode 100644 pkg/client/aws.go create mode 100644 pkg/logout/now.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36f971e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/* diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..2d42eb6 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,79 @@ +version: '3' + +vars: + BINARIES: + sh: ls cmd/ + +env: + GOOS: linux + GOARCH: amd64 + +tasks: + + default: + cmds: + - task: build-all + + build-all: + desc: build all binaries + cmds: + - for: { var: BINARIES } + task: native-compile-{{.ITEM}} + vars: + BINARY: '{{.ITEM}}' + + install-all: + desc: install all binaries + cmds: + - for: { var: BINARIES } + task: install + vars: + BINARY: '{{.ITEM}}' + + build-*: + desc: build a binary + cmd: | + mkdir -p bin + echo "Building {{.BINARY}} for {{.GOOS}}/{{.GOARCH}}" + GOOS={{.GOOS}} GOARCH={{.GOARCH}} go build -o bin/{{.BINARY}}_{{.GOOS}}_{{.GOARCH}} ./cmd/{{.BINARY}} + vars: + BINARY: '{{index .MATCH 0}}' + + native-compile-*: + desc: build a binary + cmd: | + mkdir -p bin + go build -o bin/{{.BINARY}} ./cmd/{{.BINARY}} + vars: + BINARY: '{{index .MATCH 0}}' + + cross-compile-*: + desc: cross compile a binary + cmds: + - task: build-{{.BINARY}} + vars: + GOOS: linux + GOARCH: amd64 + - task: build-{{.BINARY}} + vars: + GOOS: darwin + GOARCH: arm64 + - task: build-{{.BINARY}} + vars: + GOOS: linux + GOARCH: arm64 + vars: + BINARY: '{{index .MATCH 0}}' + + update: + desc: update dependencies + cmds: + - go get -u ./... + - go mod tidy + + install: + desc: install a binary + cmd: | + sudo install -m 0755 bin/{{.BINARY}} $DESTDIR/{{.BINARY}} + env: + DESTDIR: /usr/local/sbin diff --git a/cmd/aws-mgmt/root.go b/cmd/aws-mgmt/root.go new file mode 100644 index 0000000..87e616b --- /dev/null +++ b/cmd/aws-mgmt/root.go @@ -0,0 +1,32 @@ +package main + +import ( + "log/slog" + "os" + + cli "github.com/urfave/cli/v2" + + "aws-mgmt/internal/cmd" +) + +func main() { + + log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) + + // cli.AppHelpTemplate = HelpTemplate + + app := &cli.App{ + Usage: "AWS management commands", + Commands: []*cli.Command{ + cmd.HibernateCommand, + cmd.UpdateACommand, + cmd.SecretsCommand, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Error(err.Error()) + os.Exit(1) + } + +} diff --git a/cmd/hibernate/root.go b/cmd/hibernate/root.go new file mode 100644 index 0000000..fcdc637 --- /dev/null +++ b/cmd/hibernate/root.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "os" + + "aws-mgmt/internal/cmd" +) + +func main() { + + app := cmd.HibernateApp + + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + +} diff --git a/cmd/secrets/root.go b/cmd/secrets/root.go new file mode 100644 index 0000000..78586b5 --- /dev/null +++ b/cmd/secrets/root.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "os" + + "aws-mgmt/internal/cmd" +) + +func main() { + + app := cmd.SecretsApp + + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + +} diff --git a/cmd/update-a/root.go b/cmd/update-a/root.go new file mode 100644 index 0000000..5736489 --- /dev/null +++ b/cmd/update-a/root.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "os" + + "aws-mgmt/internal/cmd" +) + +func main() { + + app := cmd.UpdateAApp + + if err := app.Run(os.Args); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } +} diff --git a/dist/config.yaml b/dist/config.yaml new file mode 100644 index 0000000..7d22d5a --- /dev/null +++ b/dist/config.yaml @@ -0,0 +1,115 @@ +project_name: aws-mgmt-go +release: + github: + owner: jswank + name: aws-mgmt-go + name_template: '{{.Tag}}' +builds: + - id: aws-mgmt-go + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + - "386" + goarm: + - "6" + gomips: + - hardfloat + goamd64: + - v1 + targets: + - linux_amd64_v1 + - linux_arm64 + - linux_386 + - darwin_amd64_v1 + - darwin_arm64 + - windows_amd64_v1 + - windows_arm64 + - windows_386 + dir: . + main: . + binary: aws-mgmt-go + builder: go + gobinary: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser +archives: + - id: default + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}' + format: tar.gz + files: + - src: license* + - src: LICENSE* + - src: readme* + - src: README* + - src: changelog* + - src: CHANGELOG* +snapshot: + name_template: '{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}' +checksum: + name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' + algorithm: sha256 +changelog: + format: '{{ .SHA }}: {{ .Message }} ({{ with .AuthorUsername }}@{{ . }}{{ else }}{{ .AuthorName }} <{{ .AuthorEmail }}>{{ end }})' +dist: dist +env_files: + github_token: ~/.config/goreleaser/github_token + gitlab_token: ~/.config/goreleaser/gitlab_token + gitea_token: ~/.config/goreleaser/gitea_token +source: + name_template: '{{ .ProjectName }}-{{ .Version }}' + format: tar.gz +gomod: + gobinary: go +announce: + twitter: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + mastodon: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + server: "" + reddit: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + url_template: '{{ .ReleaseURL }}' + slack: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + username: GoReleaser + discord: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + author: GoReleaser + color: "3888754" + icon_url: https://goreleaser.com/static/avatar.png + teams: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + color: '#2D313E' + icon_url: https://goreleaser.com/static/avatar.png + smtp: + subject_template: '{{ .ProjectName }} {{ .Tag }} is out!' + body_template: 'You can view details from: {{ .ReleaseURL }}' + mattermost: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + username: GoReleaser + linkedin: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + telegram: + message_template: '{{ mdv2escape .ProjectName }} {{ mdv2escape .Tag }} is out{{ mdv2escape "!" }} Check it out at {{ mdv2escape .ReleaseURL }}' + parse_mode: MarkdownV2 + webhook: + message_template: '{ "message": "{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}"}' + content_type: application/json; charset=utf-8 + opencollective: + title_template: '{{ .Tag }}' + message_template: '{{ .ProjectName }} {{ .Tag }} is out!
Check it out at {{ .ReleaseURL }}' + bluesky: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' +git: + tag_sort: -version:refname +github_urls: + download: https://github.com +gitlab_urls: + download: https://gitlab.com diff --git a/dist/metadata.json b/dist/metadata.json new file mode 100644 index 0000000..518c999 --- /dev/null +++ b/dist/metadata.json @@ -0,0 +1 @@ +{"project_name":"aws-mgmt-go","tag":"v0.0.0","previous_tag":"","version":"0.0.0-SNAPSHOT-a27f612","commit":"a27f6126361f2f72af910005442de0c0701e3df2","date":"2024-11-19T09:03:30.402585616-05:00","runtime":{"goos":"linux","goarch":"amd64"}} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f0a6c2d --- /dev/null +++ b/go.mod @@ -0,0 +1,59 @@ +module aws-mgmt + +go 1.21.1 + +require ( + github.com/aws/aws-sdk-go-v2 v1.32.5 + github.com/aws/aws-sdk-go-v2/config v1.28.5 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.191.0 + github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 + github.com/charmbracelet/huh v0.6.0 + github.com/pquerna/otp v1.4.0 + github.com/urfave/cli/v2 v2.27.5 + golang.org/x/term v0.26.0 +) + +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.46 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 // indirect + github.com/aws/smithy-go v1.22.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/boombuler/barcode v1.0.2 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/bubbles v0.20.0 // indirect + github.com/charmbracelet/bubbletea v1.2.3 // indirect + github.com/charmbracelet/lipgloss v1.0.0 // indirect + github.com/charmbracelet/x/ansi v0.4.5 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20241119170951-e919f77bf23b // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0d49848 --- /dev/null +++ b/go.sum @@ -0,0 +1,116 @@ +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aws/aws-sdk-go-v2 v1.32.5 h1:U8vdWJuY7ruAkzaOdD7guwJjD06YSKmnKCJs7s3IkIo= +github.com/aws/aws-sdk-go-v2 v1.32.5/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.5 h1:Za41twdCXbuyyWv9LndXxZZv3QhTG1DinqlFsSuvtI0= +github.com/aws/aws-sdk-go-v2/config v1.28.5/go.mod h1:4VsPbHP8JdcdUDmbTVgNL/8w9SqOkM5jyY8ljIxLO3o= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46 h1:AU7RcriIo2lXjUfHFnFKYsLCwgbz1E7Mm95ieIRDNUg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.46/go.mod h1:1FmYyLGL08KQXQ6mcTlifyFXfJVCNJTVGuQP4m0d/UA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20 h1:sDSXIrlsFSFJtWKLQS4PUWRvrT580rrnuLydJrCQ/yA= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.20/go.mod h1:WZ/c+w0ofps+/OUqMwWgnfrgzZH1DZO1RIkktICsqnY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24 h1:4usbeaes3yJnCFC7kfeyhkdkPtoRYPa/hTmCqMpKpLI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.24/go.mod h1:5CI1JemjVwde8m2WG3cz23qHKPOxbpkq0HaoreEgLIY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24 h1:N1zsICrQglfzaBnrfM0Ys00860C+QFwu6u/5+LomP+o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.24/go.mod h1:dCn9HbJ8+K31i8IQ8EWmWj0EiIk0+vKiHNMxTTYveAg= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.191.0 h1:F7M5lncJ3dH6VfFohkSTBh0uRmqfB41/XxXfp8NphHI= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.191.0/go.mod h1:mzj8EEjIHSN2oZRXiw1Dd+uB4HZTl7hC8nBzX9IZMWw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5 h1:wtpJ4zcwrSbwhECWQoI/g6WM9zqCcSpHDJIWSbMLOu4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.5/go.mod h1:qu/W9HXQbbQ4+1+JcZp0ZNPV31ym537ZJN+fiS7Ti8E= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2 h1:wmt05tPp/CaRZpPV5B4SaJ5TwkHKom07/BzHoLdkY1o= +github.com/aws/aws-sdk-go-v2/service/route53 v1.46.2/go.mod h1:d+K9HESMpGb1EU9/UmmpInbGIUcAkwmcY6ZO/A3zZsw= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6 h1:1KDMKvOKNrpD667ORbZ/+4OgvUoaok1gg/MLzrHF9fw= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.6/go.mod h1:DmtyfCfONhOyVAJ6ZMTrDSFIeyCBlEO93Qkfhxwbxu0= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6 h1:3zu537oLmsPfDMyjnUS2g+F2vITgy5pB74tHI+JBNoM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.6/go.mod h1:WJSZH2ZvepM6t6jwu4w/Z45Eoi75lPN7DcydSRtJg6Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5 h1:K0OQAsDywb0ltlFrZm0JHPY3yZp/S9OaoLU33S7vPS8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.5/go.mod h1:ORITg+fyuMoeiQFiVGoqB3OydVTLkClw/ljbblMq6Cc= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLbc2iVROyaNEwBHbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +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/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4= +github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.3 h1:d9MdMsANIYZB5pE1KkRqaUV6GfsiWm+/9z4fTuGVm9I= +github.com/charmbracelet/bubbletea v1.2.3/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8= +github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU= +github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= +github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/exp/strings v0.0.0-20241119170951-e919f77bf23b h1:zj6aAhsT/hrh2RRuhkrgdwL328Akf26YjSsAArruCNE= +github.com/charmbracelet/x/exp/strings v0.0.0-20241119170951-e919f77bf23b/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +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/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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +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.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= +github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +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/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= +github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= +golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/internal/cmd/hibernate.go b/internal/cmd/hibernate.go new file mode 100644 index 0000000..8a82ed7 --- /dev/null +++ b/internal/cmd/hibernate.go @@ -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 +} diff --git a/internal/cmd/secrets.go b/internal/cmd/secrets.go new file mode 100644 index 0000000..f453c3e --- /dev/null +++ b/internal/cmd/secrets.go @@ -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 + +} diff --git a/internal/cmd/update-a.go b/internal/cmd/update-a.go new file mode 100644 index 0000000..ccd9c71 --- /dev/null +++ b/internal/cmd/update-a.go @@ -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] ", + HideHelpCommand: true, + Args: true, + ArgsUsage: "", + 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] ", + HideHelpCommand: true, + Args: true, + ArgsUsage: "", + 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 +} diff --git a/internal/cmd/util.go b/internal/cmd/util.go new file mode 100644 index 0000000..47953f5 --- /dev/null +++ b/internal/cmd/util.go @@ -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 +} diff --git a/pkg/client/aws.go b/pkg/client/aws.go new file mode 100644 index 0000000..765eef3 --- /dev/null +++ b/pkg/client/aws.go @@ -0,0 +1,265 @@ +package client + +import ( + "context" + "errors" + "fmt" + "io" + "sort" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/route53" + r53 "github.com/aws/aws-sdk-go-v2/service/route53/types" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + sm "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" +) + +type AWSClient struct { + cfg aws.Config +} + +type SecretListEntry struct { + Name string + Description string +} + +type optionFunc func(*AWSClient) + +func NewAWSClient(optionFuncs ...optionFunc) *AWSClient { + awsClient := AWSClient{} + for _, option := range optionFuncs { + option(&awsClient) + } + return &awsClient +} + +func SetConfig(cfg aws.Config) optionFunc { + return func(c *AWSClient) { + c.cfg = cfg + } +} + +/* Create config for AWS clients. If region is unset, attempt to set it via IMDS. */ +func CreateAWSConfig() (aws.Config, error) { + cfg, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return cfg, fmt.Errorf("unable to load default AWS config, %w", err) + } + + // try to set the region using IMDS if it's not already set + if cfg.Region == "" { + resp, err := imds.NewFromConfig(cfg).GetRegion(context.TODO(), &imds.GetRegionInput{}) + if err != nil { + return cfg, fmt.Errorf("unable to get AWS region from IMDS, %w", err) + } + cfg, _ = config.LoadDefaultConfig(context.TODO(), config.WithRegion(resp.Region)) + } + return cfg, nil +} + +/* Get the running instance id */ +func (c AWSClient) GetInstanceID() (string, error) { + id, err := imds.NewFromConfig(c.cfg).GetInstanceIdentityDocument(context.TODO(), &imds.GetInstanceIdentityDocumentInput{}) + if err != nil { + return "", err + } + return id.InstanceID, nil +} + +// getZoneID returns the zone id given domain +func (c AWSClient) GetZoneID(domain string) (string, error) { + r53Client := route53.NewFromConfig(c.cfg) + output, err := r53Client.ListHostedZonesByName(context.TODO(), &route53.ListHostedZonesByNameInput{ + DNSName: aws.String(domain), + }) + + if err != nil { + return "", err + } + + if len(output.HostedZones) == 0 { + return "", errors.New("no matching zone") + } + + return *output.HostedZones[0].Id, nil +} + +// GetMetadata returns the specified path from instance metadata +func (c AWSClient) GetMetadata(path string) (string, error) { + imdsClient := imds.NewFromConfig(c.cfg) + metadata, err := imdsClient.GetMetadata(context.TODO(), &imds.GetMetadataInput{ + Path: path, + }) + if err != nil { + return "", err + } + + buf := new(strings.Builder) + _, err = io.Copy(buf, metadata.Content) + if err != nil { + return "", err + } + return buf.String(), nil +} + +func (c AWSClient) StopInstance(instance_id string, hibernate bool) (string, error) { + si_out, err := ec2.NewFromConfig(c.cfg).StopInstances(context.TODO(), &ec2.StopInstancesInput{ + InstanceIds: []string{instance_id}, + Hibernate: aws.Bool(hibernate), + }) + if err != nil { + return "", err + } + return string(si_out.StoppingInstances[0].CurrentState.Name), nil +} + +func (c AWSClient) GetEC2Tag(instance_id string, tag_name string) (string, error) { + result, err := ec2.NewFromConfig(c.cfg).DescribeInstances(context.TODO(), &ec2.DescribeInstancesInput{ + InstanceIds: []string{instance_id}, + }) + if err != nil { + return "", err + } + for _, reservation := range result.Reservations { + for _, instance := range reservation.Instances { + for _, tag := range instance.Tags { + // Check if the tag key matches the one you're interested in + if *tag.Key == tag_name { + return *tag.Value, nil + } + } + } + } + return "", nil +} + +func (c AWSClient) GetEC2PublicIP(instance_id string) (string, error) { + var publicIP string + result, err := ec2.NewFromConfig(c.cfg).DescribeInstances(context.TODO(), &ec2.DescribeInstancesInput{ + InstanceIds: []string{instance_id}, + }) + if err != nil { + return "", err + } + for _, reservation := range result.Reservations { + for _, instance := range reservation.Instances { + if instance.PublicIpAddress != nil { + publicIP = *instance.PublicIpAddress + } + } + } + if publicIP == "" { + err = errors.New("no public ip address is associated with the instance") + } + return publicIP, err +} + +func (c AWSClient) UpdateA(hostname, ipaddress, zoneID string) error { + + change := r53.Change{ + Action: "UPSERT", + ResourceRecordSet: &r53.ResourceRecordSet{ + Name: aws.String(hostname), + Type: "A", + TTL: aws.Int64(60), + ResourceRecords: []r53.ResourceRecord{ + r53.ResourceRecord{Value: aws.String(ipaddress)}, + }, + }, + } + + _, err := route53.NewFromConfig(c.cfg).ChangeResourceRecordSets(context.TODO(), &route53.ChangeResourceRecordSetsInput{ + HostedZoneId: aws.String(zoneID), + ChangeBatch: &r53.ChangeBatch{ + Changes: []r53.Change{change}, + }, + }) + + return err + +} + +// ListSecrets lists all secrets with tags matching the tags map, returning a sorted list +// of secrets names and descriptions from the response. +func (c AWSClient) ListSecrets(tags map[string]string) ([]SecretListEntry, error) { + client := secretsmanager.NewFromConfig(c.cfg) + + // construct filters from tags map + var filters []sm.Filter + // filters := make([]sm.Filter, 0, len(tags)*2) + for key, value := range tags { + filters = append(filters, sm.Filter{ + Key: sm.FilterNameStringTypeTagKey, + Values: []string{key}, + }) + if value != "" { + filters = append(filters, sm.Filter{ + Key: sm.FilterNameStringTypeTagValue, + Values: []string{value}, + }) + } + } + + var secretList []SecretListEntry + + // Set the initial pagination token to an empty string + var nextToken *string + + // Loop until there are no more pages + for { + // Call ListSecrets with the pagination token + input := &secretsmanager.ListSecretsInput{ + Filters: filters, + NextToken: nextToken, + } + resp, err := client.ListSecrets(context.TODO(), input) + if err != nil { + return secretList, fmt.Errorf("unable to list secrets, %w", err) + } + + // Process the secrets in the current page + for _, secret := range resp.SecretList { + entry := SecretListEntry{ + Name: *secret.Name, + } + if secret.Description != nil { + entry.Description = *secret.Description + } + secretList = append(secretList, entry) + } + + // Check if there are more pages + if resp.NextToken == nil { + break + } + // Set the pagination token for the next iteration + nextToken = resp.NextToken + } + + // sort secretList alphabetically by name + sort.Slice(secretList, func(i, j int) bool { + return secretList[i].Name < secretList[j].Name + }) + + return secretList, nil +} + +// GetSecretValue retrieves the secret value for the given secret name. +func (c AWSClient) GetSecretValue(secretName string) (string, error) { + client := secretsmanager.NewFromConfig(c.cfg) + + input := &secretsmanager.GetSecretValueInput{ + SecretId: &secretName, + } + + resp, err := client.GetSecretValue(context.TODO(), input) + if err != nil { + return "", fmt.Errorf("unable to get secret value, %w", err) + } + + return *resp.SecretString, nil +} diff --git a/pkg/logout/now.go b/pkg/logout/now.go new file mode 100644 index 0000000..ed0565d --- /dev/null +++ b/pkg/logout/now.go @@ -0,0 +1,15 @@ +package logout + +import ( + "os" + "syscall" + + "golang.org/x/term" +) + +func Now() { + if term.IsTerminal(int(os.Stdout.Fd())) { + ppid := os.Getppid() + _ = syscall.Kill(ppid, 1) + } +}