├── .bazelignore
├── .bazeliskrc
├── .bazelrc
├── .bazelversion
├── .chglog
├── CHANGELOG.tpl.md
└── config.yml
├── .deepsource.toml
├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── .github
└── workflows
│ ├── linters.yaml
│ ├── release.yaml
│ ├── test-linux.yaml
│ └── test-macos.yaml
├── .gitignore
├── .golangci.yml
├── .pre-commit-config.yaml
├── BUILD
├── CHANGELOG.md
├── LICENSE
├── README.md
├── WORKSPACE
├── cmd
├── BUILD.bazel
├── list.go
├── root.go
├── session.go
├── ssh.go
└── verify.go
├── docs
├── 00-get-started.md
├── 10-configuration.md
├── README.md
└── usage
│ ├── 00-list.md
│ ├── 10-session.md
│ ├── 20-ssh.md
│ ├── 30-verify.md
│ └── README.md
├── go.mod
├── go.sum
├── main.go
├── pkg
├── aws
│ ├── BUILD.bazel
│ ├── aws.go
│ ├── aws_test.go
│ ├── helpers
│ │ ├── BUILD.bazel
│ │ └── helpers.go
│ ├── interface.go
│ ├── list.go
│ ├── list_test.go
│ ├── log
│ │ ├── BUILD.bazel
│ │ └── log.go
│ ├── session.go
│ ├── session_test.go
│ ├── ssh.go
│ └── ssh_test.go
├── list
│ ├── BUILD.bazel
│ ├── list.go
│ └── list_test.go
├── os
│ ├── BUILD.bazel
│ ├── ignore_signals.go
│ └── ignore_signals_windows.go
├── session
│ ├── BUILD.bazel
│ ├── session.go
│ └── session_test.go
└── ssh
│ ├── BUILD.bazel
│ ├── ssh.go
│ └── ssh_test.go
├── renovate.json
├── scripts
└── pre-commit.sh
└── tools
├── BUILD
├── fix_codecov.sh
├── get_workspace_status.sh
└── repositories.bzl
/.bazelignore:
--------------------------------------------------------------------------------
1 | .git
2 | scripts/
3 |
--------------------------------------------------------------------------------
/.bazeliskrc:
--------------------------------------------------------------------------------
1 | BAZELISK_NOJDK=1
2 |
--------------------------------------------------------------------------------
/.bazelrc:
--------------------------------------------------------------------------------
1 | startup --expand_configs_in_place
2 |
3 | build --collect_code_coverage
4 |
5 | # Show us information about failures.
6 | build --verbose_failures
7 | test --test_output=errors
8 |
9 | # Include git version and other info
10 | build --stamp
11 | build --workspace_status_command tools/get_workspace_status.sh
12 |
13 | # Make /tmp hermetic
14 | build --sandbox_tmpfs_path=/tmp
15 |
16 | # Ensure that Bazel never runs as root, which can cause unit tests to fail.
17 | build --sandbox_fake_username
18 |
19 | test:unit --build_tests_only
20 |
21 | # Cross-compile pure Go
22 | build:cross:darwin_amd64 --platforms=@io_bazel_rules_go//go/toolchain:darwin_amd64 --cpu=amd64
23 | build:cross:windows_amd64 --platforms=@io_bazel_rules_go//go/toolchain:windows_amd64 --cpu=amd64
24 | build:cross:linux_amd64 --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 --cpu=amd64
25 |
--------------------------------------------------------------------------------
/.bazelversion:
--------------------------------------------------------------------------------
1 | 4.2.1
2 |
--------------------------------------------------------------------------------
/.chglog/CHANGELOG.tpl.md:
--------------------------------------------------------------------------------
1 | {{ if .Versions -}}
2 |
3 | ## [Unreleased]
4 |
5 | {{ if .Unreleased.CommitGroups -}}
6 | {{ range .Unreleased.CommitGroups -}}
7 | ### {{ .Title }}
8 | {{ range .Commits -}}
9 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
10 | {{ end }}
11 | {{ end -}}
12 | {{ end -}}
13 | {{ end -}}
14 |
15 | {{ range .Versions }}
16 |
17 | ## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
18 | {{ range .CommitGroups -}}
19 | ### {{ .Title }}
20 | {{ range .Commits -}}
21 | - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
22 | {{ end }}
23 | {{ end -}}
24 |
25 | {{- if .RevertCommits -}}
26 | ### Reverts
27 | {{ range .RevertCommits -}}
28 | - {{ .Revert.Header }}
29 | {{ end }}
30 | {{ end -}}
31 |
32 | {{- if .NoteGroups -}}
33 | {{ range .NoteGroups -}}
34 | ### {{ .Title }}
35 | {{ range .Notes }}
36 | {{ .Body }}
37 | {{ end }}
38 | {{ end -}}
39 | {{ end -}}
40 | {{ end -}}
41 |
42 | {{- if .Versions }}
43 | [Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD
44 | {{ range .Versions -}}
45 | {{ if .Tag.Previous -}}
46 | [{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
47 | {{ end -}}
48 | {{ end -}}
49 | {{ end -}}
50 |
--------------------------------------------------------------------------------
/.chglog/config.yml:
--------------------------------------------------------------------------------
1 | style: github
2 | template: CHANGELOG.tpl.md
3 | info:
4 | title: CHANGELOG
5 | repository_url: https://github.com/danmx/sigil
6 | options:
7 | commits:
8 | # filters:
9 | # Type:
10 | # - feat
11 | # - fix
12 | # - perf
13 | # - refactor
14 | commit_groups:
15 | # title_maps:
16 | # feat: Features
17 | # fix: Bug Fixes
18 | # perf: Performance Improvements
19 | # refactor: Code Refactoring
20 | header:
21 | pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
22 | pattern_maps:
23 | - Type
24 | - Scope
25 | - Subject
26 | notes:
27 | keywords:
28 | - BREAKING CHANGE
29 |
--------------------------------------------------------------------------------
/.deepsource.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | test_patterns = ["**/*_test.go"]
4 | exclude_patterns = ["**/*_mock_test.go"]
5 |
6 | [[analyzers]]
7 | name = "go"
8 | enabled = true
9 |
10 | [analyzers.meta]
11 | import_path = "github.com/danmx/sigil"
12 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:21.04
2 |
3 | ARG BAZELISK_SHA256_HASH="4cb534c52cdd47a6223d4596d530e7c9c785438ab3b0a49ff347e991c210b2cd"
4 | ARG BAZELISK_VERSION="v1.10.1"
5 |
6 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -q -y \
7 | python \
8 | python3 \
9 | wget \
10 | ca-certificates \
11 | gcc \
12 | git \
13 | && apt-get clean \
14 | && wget -q -O /tmp/bazelisk https://github.com/bazelbuild/bazelisk/releases/download/${BAZELISK_VERSION}/bazelisk-linux-amd64 \
15 | && echo "${BAZELISK_SHA256_HASH} /tmp/bazelisk" | sha256sum -c - \
16 | && mv /tmp/bazelisk /usr/local/bin/bazel \
17 | && chmod +x /usr/local/bin/bazel
18 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sigil",
3 | "build": {
4 | "dockerfile": "Dockerfile"
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/.github/workflows/linters.yaml:
--------------------------------------------------------------------------------
1 | name: Linters
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | linters:
7 | name: Linters
8 | runs-on: ${{ matrix.os }}
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | os:
13 | - ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: bazelbuild/setup-bazelisk@v1
17 | - name: Mount bazel cache # Optional
18 | uses: actions/cache@v2
19 | with:
20 | path: "~/.cache/bazel"
21 | key: bazel
22 | - name: gofmt
23 | run: bazel run :gazelle && bazel run @go_sdk//:bin/gofmt -- -e -l .
24 | shell: bash
25 | - name: golangci-lint
26 | run: bazel run @com_github_danmx_bazel_tools//golangci-lint:run -- run ./...
27 | shell: bash
28 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Bazel Release
2 |
3 | defaults:
4 | run:
5 | shell: bash
6 |
7 | on:
8 | push:
9 | tags:
10 | - '*'
11 |
12 | jobs:
13 | release:
14 | name: Release
15 | runs-on: ${{ matrix.os }}
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | os:
20 | - ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Mount bazel cache # Optional
24 | uses: actions/cache@v2
25 | with:
26 | path: "~/.cache/bazel"
27 | key: bazel
28 | - name: Login to Docker Hub
29 | uses: docker/login-action@v1
30 | with:
31 | username: ${{ secrets.DOCKERHUB_USERNAME }}
32 | password: ${{ secrets.DOCKERHUB_TOKEN }}
33 | - uses: bazelbuild/setup-bazelisk@v1
34 | - name: Run build release binaries
35 | run: |
36 | bazel build --config cross:linux_amd64 :sigil_linux-amd64
37 | bazel build --config cross:windows_amd64 :sigil_windows-amd64
38 | bazel build --config cross:darwin_amd64 :sigil_darwin-amd64
39 | - name: Run build release container image
40 | run: |
41 | bazel run --config cross:linux_amd64 :push-release-image
42 | bazel run --config cross:linux_amd64 :push-major-release-image
43 | bazel run --config cross:linux_amd64 :push-minor-release-image
44 | - name: Release
45 | uses: softprops/action-gh-release@v1
46 | with:
47 | files: |
48 | bazel-bin/sigil_linux-amd64.tar.gz
49 | bazel-bin/sigil_darwin-amd64.tar.gz
50 | bazel-bin/sigil_windows-amd64.zip
51 |
--------------------------------------------------------------------------------
/.github/workflows/test-linux.yaml:
--------------------------------------------------------------------------------
1 | name: Bazel Linux Tests
2 |
3 | defaults:
4 | run:
5 | shell: bash
6 |
7 | on: [push, pull_request]
8 |
9 | jobs:
10 | tests:
11 | name: Tests
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os:
17 | - ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Mount bazel cache # Optional
21 | uses: actions/cache@v2
22 | with:
23 | path: "~/.cache/bazel"
24 | key: bazel
25 | - name: Login to Docker Hub
26 | uses: docker/login-action@v1
27 | if: ${{ github.event_name != 'pull_request' }}
28 | with:
29 | username: ${{ secrets.DOCKERHUB_USERNAME }}
30 | password: ${{ secrets.DOCKERHUB_TOKEN }}
31 | - uses: bazelbuild/setup-bazelisk@v1
32 | - name: Run tests
33 | run: bazel test --config cross:linux_amd64 //...
34 | - name: Build
35 | run: bazel build --config cross:linux_amd64 :dev
36 | - name: Build container image
37 | run: bazel build --config cross:linux_amd64 :dev-image
38 | - name: Push dev container image
39 | run: bazel run --config cross:linux_amd64 :push-dev-image
40 | if: ${{ github.event_name != 'pull_request' }}
41 | - uses: codecov/codecov-action@v2
42 | with:
43 | files: ./coverage.txt
44 | directory: ./bazel-bin/
45 | verbose: true
46 |
--------------------------------------------------------------------------------
/.github/workflows/test-macos.yaml:
--------------------------------------------------------------------------------
1 | name: Bazel MacOS Tests
2 |
3 | defaults:
4 | run:
5 | shell: bash
6 |
7 | on: [pull_request]
8 |
9 | jobs:
10 | tests:
11 | name: Tests
12 | runs-on: ${{ matrix.os }}
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os:
17 | - macos-latest
18 | - macos-10.15
19 | - macos-11.0
20 | steps:
21 | - uses: actions/checkout@v2
22 | - name: Mount bazel cache # Optional
23 | uses: actions/cache@v2
24 | with:
25 | path: "~/.cache/bazel"
26 | key: bazel
27 | - uses: bazelbuild/setup-bazelisk@v1
28 | - name: Run tests
29 | run: bazel test --config cross:darwin_amd64 //...
30 | - name: Build
31 | run: bazel build --config cross:darwin_amd64 :dev
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor/
2 | bin/
3 | dist/
4 | coverage.txt
5 | *.deb
6 | bazel-*
7 | *mock_test.go
8 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | deadline: 1m
3 | tests: true
4 | skip-dirs:
5 | - .git
6 | - bazel-*
7 | - tools
8 | - docs
9 | skip-files:
10 | - ".*_test\\.go$"
11 | skip-dirs-use-default: true
12 | modules-download-mode: readonly
13 | allow-parallel-runners: true
14 | linters-settings:
15 | dupl:
16 | threshold: 100
17 | goconst:
18 | min-len: 2
19 | min-occurrences: 2
20 | gocritic:
21 | enabled-tags:
22 | - diagnostic
23 | - experimental
24 | - opinionated
25 | - performance
26 | - style
27 | disabled-checks:
28 | - dupImport # https://github.com/go-critic/go-critic/issues/845
29 | - ifElseChain
30 | - octalLiteral
31 | - wrapperFunc
32 | gocyclo:
33 | min-complexity: 15
34 | goimports:
35 | local-prefixes: github.com/danmx/sigil
36 | golint:
37 | min-confidence: 0
38 | gomnd:
39 | settings:
40 | mnd:
41 | checks: argument,case,condition,return,operation,assign
42 | govet:
43 | check-shadowing: true
44 | maligned:
45 | suggest-new: true
46 | misspell:
47 | locale: UK
48 |
49 | linters:
50 | disable-all: true
51 | enable:
52 | - bodyclose
53 | - deadcode
54 | - depguard
55 | - dogsled
56 | - dupl
57 | - errcheck
58 | - goconst
59 | - gocritic
60 | - gocyclo
61 | - gofmt
62 | - goimports
63 | - gomnd
64 | - goprintffuncname
65 | - gosec
66 | - gosimple
67 | - govet
68 | - ineffassign
69 | - misspell
70 | - nakedret
71 | - staticcheck
72 | - structcheck
73 | - stylecheck
74 | - typecheck
75 | - unconvert
76 | - unparam
77 | - unused
78 | - varcheck
79 | - whitespace
80 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | default_stages: [commit]
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v3.1.0
5 | hooks:
6 | - id: check-yaml
7 | args:
8 | - --allow-multiple-documents
9 | - id: check-toml
10 | - id: end-of-file-fixer
11 | - id: trailing-whitespace
12 | - repo: local
13 | hooks:
14 | - id: fmt
15 | name: Run formatter
16 | entry: scripts/pre-commit.sh fmt
17 | language: system
18 | require_serial: true
19 | files: '.*\.go$'
20 | - id: lint
21 | name: Run linter
22 | entry: scripts/pre-commit.sh lint
23 | language: system
24 | require_serial: true
25 | files: '.*\.go$'
26 | stages:
27 | - push
28 | - id: update_deps
29 | name: Update Go dependencies in Bazel
30 | entry: scripts/pre-commit.sh update_deps
31 | language: system
32 | require_serial: true
33 | files: 'go\.(mod|sum)$'
34 | stages:
35 | - push
36 | - id: test
37 | name: Run tests
38 | entry: scripts/pre-commit.sh test
39 | language: system
40 | require_serial: true
41 | files: '.*\.go$'
42 | stages:
43 | - push
44 | - id: changelog
45 | name: Generate the changelog
46 | entry: scripts/pre-commit.sh changelog
47 | language: system
48 | require_serial: true
49 | stages:
50 | - push
51 |
--------------------------------------------------------------------------------
/BUILD:
--------------------------------------------------------------------------------
1 | # gazelle:prefix github.com/danmx/sigil
2 | # gazelle:proto disable_global
3 |
4 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
5 | load("@bazel_gazelle//:def.bzl", "gazelle")
6 | load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar")
7 | load("@io_bazel_rules_docker//container:image.bzl", "container_image")
8 | load("@io_bazel_rules_docker//container:layer.bzl", "container_layer")
9 | load("@io_bazel_rules_docker//docker:docker.bzl", "docker_push")
10 |
11 | package(default_visibility = ["//visibility:public"])
12 |
13 | genrule(
14 | name = "concat-cov",
15 | srcs = glob(["bazel-out/**/testlogs/**/coverage.dat"]),
16 | outs = ["coverage.txt"],
17 | cmd_bash = "./$(location //tools:fix_codecov.sh) > \"$@\"",
18 | exec_tools = ["//tools:fix_codecov.sh"],
19 | )
20 |
21 | gazelle(
22 | name = "gazelle",
23 | command = "fix",
24 | )
25 |
26 | go_library(
27 | name = "go_default_library",
28 | srcs = ["main.go"],
29 | importpath = "github.com/danmx/sigil",
30 | visibility = ["//visibility:private"],
31 | x_defs = {
32 | "github.com/danmx/sigil/cmd.gitCommit": "{STABLE_GIT_COMMIT}",
33 | "github.com/danmx/sigil/cmd.appVersion": "{STABLE_VERSION}",
34 | },
35 | deps = ["//cmd:go_default_library"],
36 | )
37 |
38 | # Development
39 | go_binary(
40 | name = "dev",
41 | out = "dev/sigil",
42 | embed = [":go_default_library"],
43 | pure = "on",
44 | static = "on",
45 | x_defs = {
46 | "github.com/danmx/sigil/cmd.gitCommit": "{STABLE_GIT_COMMIT}",
47 | "github.com/danmx/sigil/cmd.appVersion": "{STABLE_VERSION}",
48 | "github.com/danmx/sigil/cmd.dev": "true",
49 | "github.com/danmx/sigil/cmd.logLevel": "debug",
50 | },
51 | )
52 |
53 | # Release
54 | go_binary(
55 | name = "release",
56 | out = "sigil",
57 | embed = [":go_default_library"],
58 | pure = "on",
59 | static = "on",
60 | )
61 |
62 | pkg_tar(
63 | name = "sigil_darwin-amd64",
64 | srcs = [":release"],
65 | extension = "tar.gz",
66 | mode = "0o755",
67 | )
68 |
69 | pkg_tar(
70 | name = "sigil_linux-amd64",
71 | srcs = [":release"],
72 | extension = "tar.gz",
73 | mode = "0o755",
74 | )
75 |
76 | pkg_tar(
77 | name = "sigil_windows-amd64",
78 | srcs = [":release"],
79 | extension = "zip",
80 | mode = "0o755",
81 | )
82 |
83 | # Include it in our base image as a tar.
84 | container_layer(
85 | name = "plugin-layer",
86 | debs = ["@session_manager_plugin_deb//file"],
87 | symlinks = {"/usr/bin/session-manager-plugin": "/usr/local/sessionmanagerplugin/bin/session-manager-plugin"},
88 | visibility = ["//visibility:private"],
89 | )
90 |
91 | container_layer(
92 | name = "dev-layer",
93 | directory = "/usr/bin",
94 | files = [":dev"],
95 | visibility = ["//visibility:private"],
96 | )
97 |
98 | container_layer(
99 | name = "release-layer",
100 | files = [":release"],
101 | visibility = ["//visibility:private"],
102 | )
103 |
104 | container_image(
105 | name = "dev-image",
106 | base = "@go_debug_image_base//image",
107 | cmd = ["--help"],
108 | entrypoint = ["sigil"],
109 | layers = [
110 | "plugin-layer",
111 | "dev-layer",
112 | ],
113 | user = "nonroot",
114 | )
115 |
116 | container_image(
117 | name = "release-image",
118 | base = "@go_debug_image_base//image",
119 | cmd = ["--help"],
120 | entrypoint = ["sigil"],
121 | layers = [
122 | "plugin-layer",
123 | "release-layer",
124 | ],
125 | user = "nonroot",
126 | )
127 |
128 | docker_push(
129 | name = "push-dev-image",
130 | image = ":dev-image",
131 | registry = "docker.io",
132 | repository = "danmx/sigil",
133 | tag = "dev",
134 | )
135 |
136 | docker_push(
137 | name = "push-release-image",
138 | image = ":release-image",
139 | registry = "docker.io",
140 | repository = "danmx/sigil",
141 | tag = "{STABLE_VERSION}",
142 | )
143 |
144 | docker_push(
145 | name = "push-major-release-image",
146 | image = ":release-image",
147 | registry = "docker.io",
148 | repository = "danmx/sigil",
149 | tag = "{STABLE_MAJOR_VERSION}",
150 | )
151 |
152 | docker_push(
153 | name = "push-minor-release-image",
154 | image = ":release-image",
155 | registry = "docker.io",
156 | repository = "danmx/sigil",
157 | tag = "{STABLE_MINOR_VERSION}",
158 | )
159 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [Unreleased]
3 |
4 | ### Bazel
5 | - updating Go to 1.17.2
6 | - update gomock to version 1.3
7 | - updating rules_docker to 0.19.0
8 | - update gazelle to 0.23.0
9 | - update rules_go to version 0.28.0
10 | - update to version 4.2.1
11 |
12 | ### Chore
13 | - refactoring dev tools
14 | - adding VSCode dev container
15 | - **bazel:** removing reduntant rules_docker bits
16 | - **deps:** updating Go dependencies
17 | - **deps:** update dependency bazel_gazelle to v0.22.0
18 | - **deps:** update dependency io_bazel_rules_docker to v0.15.0
19 | - **deps:** updating AWS session-manager-plugin to version 1.2.7.0
20 | - **deps:** update l.gcr.io/google/bazel docker tag to v3.5.0
21 | - **deps:** update dependency io_bazel_rules_go to v0.24.5
22 | - **deps:** update module spf13/cobra to v1.1.1
23 | - **deps:** update module spf13/cobra to v1.1.0
24 | - **deps:** update module sirupsen/logrus to v1.7.0
25 | - **deps:** bumping go version to 1.15.3
26 | - **deps:** update dependency io_bazel_rules_go to v0.24.4
27 | - **deps:** updating Bazel rules
28 | - **deps:** update dependency bazel_gazelle to v0.22.2
29 | - **deps:** update module aws/aws-sdk-go to v1.34.32
30 | - **deps:** update dependency io_bazel_rules_go to v0.24.3
31 | - **deps:** update dependency bazel_gazelle to v0.22.1
32 | - **deps:** update dependency io_bazel_rules_go to v0.24.2
33 | - **deps:** update dependency io_bazel_rules_go to v0.24.1
34 | - **renovate:** scheduled checks
35 | - **tools:** updating dev tools
36 |
37 | ### Ci
38 | - using Github Actions
39 |
40 | ### Docker
41 | - update session manager plugin to 1.2.245.0
42 |
43 | ### Fic
44 | - **ci:** removing Drone integration
45 |
46 | ### Fix
47 | - profile to command line parser
48 | - **aws:** code style of log interface
49 | - **aws:** log interface
50 | - **bazel:** semi-hermitizing Go SDK
51 | - **ci:** disable steps that use secrets on PRs ([#187](https://github.com/danmx/sigil/issues/187))
52 | - **drone:** updating drone.yml signature
53 | - **linters:** addressing golangci-lint issues
54 |
55 | ### Test
56 | - **aws:** adding unittests
57 |
58 | ### Update
59 | - **go:** version 1.15.2
60 | - **go:** version 1.15.1
61 |
62 |
63 |
64 | ## [0.7.0] - 2020-08-27
65 | ### Chore
66 | - **release:** version 0.7.0
67 |
68 | ### Feat
69 | - **cmd:** moving target to arg instead of separate flag
70 |
71 | ### Fix
72 | - **aws:** error log on a send ssh pub key failure
73 | - **cmd:** clarify ssh usage
74 | - **docs:** adding profile config entry description
75 |
76 |
77 |
78 | ## [0.6.1] - 2020-08-19
79 | ### Chore
80 | - **release:** version 0.6.1
81 |
82 | ### Fix
83 | - **aws:** reducing timeouts to speed up error feedback loop
84 |
85 |
86 |
87 | ## [0.6.0] - 2020-08-16
88 | ### Aws
89 | - customizing default retryer for AWS api calls
90 |
91 | ### Chore
92 | - removing deprecated name tag reference
93 | - **deps:** update golang.org/x/crypto commit hash to 75b2880
94 | - **deps:** update Go to 1.15
95 | - **deps:** bumping Go and tidying dependecies
96 | - **deps:** update module aws/aws-sdk-go to v1.34.2
97 | - **deps:** update dependency io_bazel_rules_go to v0.23.7
98 | - **deps:** update module spf13/viper to v1.7.1
99 | - **deps:** update module golang/mock to v1.4.4
100 | - **deps:** update golang.org/x/crypto commit hash to 123391f
101 | - **deps:** update l.gcr.io/google/bazel docker tag to v3.4.1
102 | - **deps:** update dependency io_bazel_rules_docker to v0.14.4
103 | - **deps:** x/crypto version bump
104 | - **release:** version 0.6.0
105 |
106 | ### Fix
107 | - **bazel:** docker rules
108 | - **pre-commit:** adding Gazelle run during fmt phase
109 |
110 |
111 |
112 | ## [0.5.3] - 2020-06-21
113 | ### Chore
114 | - **bazel:** bump to version 3.3.0
115 | - **deps:** update go modules in Bazel
116 | - **deps:** update module aws/aws-sdk-go to v1.32.6
117 | - **deps:** update l.gcr.io/google/bazel docker tag to v3.3.0
118 | - **pre-commit:** add go mod tidy to update_deps
119 | - **release:** version 0.5.3
120 |
121 | ### Rollback
122 | - name-tag target type change
123 |
124 | ### Update
125 | - **go:** version 1.14.4
126 |
127 |
128 |
129 | ## [0.5.2] - 2020-06-06
130 | ### Build
131 | - **deps:** bump github.com/aws/aws-sdk-go from 1.31.7 to 1.31.11
132 | - **deps:** bump github.com/aws/aws-sdk-go from 1.31.1 to 1.31.7
133 | - **deps:** bump github.com/stretchr/testify from 1.5.1 to 1.6.0
134 |
135 | ### Chore
136 | - **deps:** add renovate.json
137 | - **deps:** update module stretchr/testify to v1.6.1
138 | - **deps:** update dependency io_bazel_rules_go to v0.23.3
139 | - **deps:** update dependency io_bazel_rules_docker to v0.14.3
140 | - **deps:** update dependency bazel_gazelle to v0.21.1
141 | - **deps:** update golang.org/x/crypto commit hash to 70a84ac
142 | - **release:** 0.5.2
143 |
144 | ### Fix
145 | - **ssh:** error handling
146 |
147 | ### Update
148 | - **bazel:** to version 3.2.0
149 | - **deps:** Go dependecies in bazel
150 |
151 |
152 |
153 | ## [0.5.1] - 2020-05-23
154 | ### Add
155 | - **aws:** append AWS UA with sigil version
156 |
157 | ### Build
158 | - **deps:** bump github.com/aws/aws-sdk-go from 1.31.0 to 1.31.1
159 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.29 to 1.31.0
160 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.28 to 1.30.29
161 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.27 to 1.30.28
162 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.26 to 1.30.27
163 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.25 to 1.30.26
164 | - **deps:** bump gopkg.in/yaml.v2 from 2.2.8 to 2.3.0
165 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.24 to 1.30.25
166 | - **deps:** bump github.com/spf13/viper from 1.6.3 to 1.7.0
167 | - **deps:** bump github.com/aws/aws-sdk-go from 1.30.9 to 1.30.24
168 | - **deps:** bump github.com/sirupsen/logrus from 1.5.0 to 1.6.0
169 |
170 | ### Chore
171 | - using Bazel as a build system
172 | - **release:** 0.5.1
173 |
174 | ### Fix
175 | - app exit on failed session termination
176 | - version in makefile
177 | - **drone:** docker-release step
178 | - **lint:** addressing comments from deepsource.io
179 |
180 | ### Rm
181 | - **changelog:** merge PR commits
182 | - **drone:** release notes
183 |
184 | ### Update
185 | - dependencies
186 |
187 |
188 |
189 | ## [0.5.0] - 2020-04-18
190 | ### Chore
191 | - **pre-commit:** always run
192 |
193 | ### Delete
194 | - **doc:** manual pages
195 |
196 | ### Feat
197 | - adding support for environment variables
198 | - **ssh:** allowing custom temp. key directories
199 |
200 | ### Fix
201 | - **doc:** adding missing flag in ssh_config example
202 | - **lint:** removing broken bin path
203 |
204 |
205 |
206 | ## [0.4.1] - 2020-04-16
207 | ### Fix
208 | - **list:** filters
209 |
210 | ### Update
211 | - **version:** 0.4.1
212 |
213 |
214 |
215 | ## [0.4.0] - 2020-04-11
216 | ### Add
217 | - deepsource integration
218 | - **desc:** for tests
219 | - **pkg:** unit tests
220 | - **pre-commit:** adding pre-commit and dev section
221 |
222 | ### Feat
223 | - **golangci-lint:** added config file
224 |
225 | ### Fix
226 | - **ssh:** adding missing mfa token
227 |
228 | ### Update
229 | - **doc:** expanding documentation
230 | - **go:** bumping go and dependencies versions
231 | - **version:** to 0.4.0
232 |
233 |
234 |
235 | ## [0.3.3] - 2020-03-19
236 |
237 |
238 | ## [0.3.2] - 2020-03-11
239 |
240 |
241 | ## [0.3.1] - 2019-07-18
242 |
243 |
244 | ## [0.3.0] - 2019-07-13
245 | ### Stargate
246 | - Adding Support for SSH and SCP ([#44](https://github.com/danmx/sigil/issues/44))
247 |
248 |
249 |
250 | ## [0.2.1] - 2019-05-14
251 |
252 |
253 | ## [0.2.0] - 2019-05-03
254 |
255 |
256 | ## [0.1.2] - 2019-04-29
257 |
258 |
259 | ## [0.1.1] - 2019-04-23
260 |
261 |
262 | ## [0.1.0] - 2019-04-23
263 |
264 |
265 | ## [0.0.8] - 2019-04-22
266 |
267 |
268 | ## [0.0.7] - 2019-04-16
269 |
270 |
271 | ## [0.0.6] - 2019-04-16
272 |
273 |
274 | ## [0.0.5] - 2019-04-15
275 |
276 |
277 | ## [0.0.4] - 2019-04-15
278 |
279 |
280 | ## [0.0.3] - 2019-03-19
281 |
282 |
283 | ## [0.0.2] - 2019-03-19
284 |
285 |
286 | ## 0.0.1 - 2019-03-18
287 |
288 | [Unreleased]: https://github.com/danmx/sigil/compare/0.7.0...HEAD
289 | [0.7.0]: https://github.com/danmx/sigil/compare/0.6.1...0.7.0
290 | [0.6.1]: https://github.com/danmx/sigil/compare/0.6.0...0.6.1
291 | [0.6.0]: https://github.com/danmx/sigil/compare/0.5.3...0.6.0
292 | [0.5.3]: https://github.com/danmx/sigil/compare/0.5.2...0.5.3
293 | [0.5.2]: https://github.com/danmx/sigil/compare/0.5.1...0.5.2
294 | [0.5.1]: https://github.com/danmx/sigil/compare/0.5.0...0.5.1
295 | [0.5.0]: https://github.com/danmx/sigil/compare/0.4.1...0.5.0
296 | [0.4.1]: https://github.com/danmx/sigil/compare/0.4.0...0.4.1
297 | [0.4.0]: https://github.com/danmx/sigil/compare/0.3.3...0.4.0
298 | [0.3.3]: https://github.com/danmx/sigil/compare/0.3.2...0.3.3
299 | [0.3.2]: https://github.com/danmx/sigil/compare/0.3.1...0.3.2
300 | [0.3.1]: https://github.com/danmx/sigil/compare/0.3.0...0.3.1
301 | [0.3.0]: https://github.com/danmx/sigil/compare/0.2.1...0.3.0
302 | [0.2.1]: https://github.com/danmx/sigil/compare/0.2.0...0.2.1
303 | [0.2.0]: https://github.com/danmx/sigil/compare/0.1.2...0.2.0
304 | [0.1.2]: https://github.com/danmx/sigil/compare/0.1.1...0.1.2
305 | [0.1.1]: https://github.com/danmx/sigil/compare/0.1.0...0.1.1
306 | [0.1.0]: https://github.com/danmx/sigil/compare/0.0.8...0.1.0
307 | [0.0.8]: https://github.com/danmx/sigil/compare/0.0.7...0.0.8
308 | [0.0.7]: https://github.com/danmx/sigil/compare/0.0.6...0.0.7
309 | [0.0.6]: https://github.com/danmx/sigil/compare/0.0.5...0.0.6
310 | [0.0.5]: https://github.com/danmx/sigil/compare/0.0.4...0.0.5
311 | [0.0.4]: https://github.com/danmx/sigil/compare/0.0.3...0.0.4
312 | [0.0.3]: https://github.com/danmx/sigil/compare/0.0.2...0.0.3
313 | [0.0.2]: https://github.com/danmx/sigil/compare/0.0.1...0.0.2
314 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sigil
2 |
3 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fdanmx%2Fsigil?ref=badge_shield)
4 | [](https://cloud.drone.io/danmx/sigil)
5 | [](https://www.codacy.com/app/danmx/sigil?utm_source=github.com&utm_medium=referral&utm_content=danmx/sigil&utm_campaign=Badge_Grade)
6 | [](https://codecov.io/gh/danmx/sigil)
7 | [](https://deepsource.io/gh/danmx/sigil/?ref=repository-badge)
8 |
9 | ## Description
10 |
11 | > *Sigil* is the hub of the Great Wheel, a city at the center of the Outlands, the most balanced of neutral areas at the center of the planes. Also known as the "City of Doors" for the multitude of portals to other planes of existence and the Cage since those portals are the only way in or out, it is the setting for most of Planescape: Torment.
12 |
13 | *Sigil* is an AWS SSM Session manager client. Allowing access to EC2 instances without exposing any ports.
14 |
15 | ## Features
16 |
17 | - configuration files support (TOML, YAML, JSON, etc.)
18 | - support for different configuration profiles
19 | - lightweight [container image](https://hub.docker.com/r/danmx/sigil)
20 | - SSH and SCP support
21 |
22 | ## External dependencies
23 |
24 | ### Local
25 |
26 | - AWS [session-manager-plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) (version 1.1.17.0+ for SSH support)
27 |
28 | ### Remote
29 |
30 | - target EC2 instance must have AWS SSM Agent installed ([full guide](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html)) (version 2.3.672.0+ for SSH support)
31 | - AWS [ec2-instance-connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-set-up.html) to use SSH with your own and/or temporary keys
32 | - to support AWS SSM target EC2 instance profile should have **AmazonSSMManagedInstanceCore** managed IAM policy attached or a specific policy with similar permissions (check [About Policies for a Systems Manager Instance Profile](https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-profile.html) and [About Minimum S3 Bucket Permissions for SSM Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-minimum-s3-permissions.html))
33 |
34 | ## Documentation
35 |
36 | The manual can be found [here](docs/README.md).
37 |
38 | ## Installation
39 |
40 | ### Homebrew
41 |
42 | ```shell
43 | brew tap danmx/sigil
44 | brew install sigil
45 | ```
46 |
47 | or
48 |
49 | ```shell
50 | brew install danmx/sigil/sigil
51 | ```
52 |
53 | ### Docker
54 |
55 | ```shell
56 | docker pull danmx/sigil:0.7
57 | ```
58 |
59 | ## Examples
60 |
61 | ### Usage
62 |
63 | Docker:
64 |
65 | ```shell
66 | docker run --rm -it -v "${HOME}"/.sigil:/home/nonroot/.sigil -v "${HOME}"/.aws:/home/.aws danmx/sigil:0.7 list --output-format wide
67 | ```
68 |
69 | Binary:
70 |
71 | ```shell
72 | sigil -r eu-west-1 session --type instance-id i-xxxxxxxxxxxxxxxxx
73 | ```
74 |
75 | Using with [aws-vault](https://github.com/99designs/aws-vault):
76 |
77 | ```shell
78 | aws-vault exec AWS_PROFILE -- sigil -r eu-west-1 session --type instance-id i-xxxxxxxxxxxxxxxxx
79 | ```
80 |
81 | ### SSH integration
82 |
83 | Add an entry to your `ssh_config`:
84 |
85 | ```ssh_config
86 | Host i-* mi-*
87 | IdentityFile /tmp/sigil/%h/temp_key
88 | IdentitiesOnly yes
89 | ProxyCommand sigil ssh --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h
90 | Host *.compute.internal
91 | IdentityFile /tmp/sigil/%h/temp_key
92 | IdentitiesOnly yes
93 | ProxyCommand sigil ssh --type private-dns --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h
94 | ```
95 |
96 | and run:
97 |
98 | ```shell
99 | ssh ec2-user@i-123456789
100 | ```
101 |
102 | or
103 |
104 | ```shell
105 | ssh ec2-user@ip-10-0-0-5.eu-west-1.compute.internal
106 | ```
107 |
108 | ### Config file
109 |
110 | By default configuration file is located in `${HOME}/.sigil/config.toml`.
111 |
112 | ```toml
113 | [default]
114 | type = "instance-id"
115 | output-format = "wide"
116 | region = "eu-west-1"
117 | profile = "dev"
118 | interactive = true
119 | ```
120 |
121 | ## Changelog
122 |
123 | See [CHANGELOG.md](CHANGELOG.md)
124 |
125 | ## Build
126 |
127 | ### Binaries
128 |
129 | To build binaries (`development` and `release`) run:
130 |
131 | ```shell
132 | bazelisk build //...
133 | ```
134 |
135 | To run specific build use:
136 |
137 | ```shell
138 | bazelisk build --config cross:[darwin|linux|windows]_amd64 :[dev|release]
139 | ```
140 |
141 | for working Docker image:
142 |
143 | ```shell
144 | bazelisk build --config cross:linux_amd64 :[dev|release]-image
145 | ```
146 |
147 | ### Container image
148 |
149 | To only build docker image run:
150 |
151 | ```shell
152 | bazelisk run :dev-image
153 | ```
154 |
155 | It'll create a docker image tagged `bazel:dev-image`.
156 |
157 | ## Contributions
158 |
159 | All contributions are welcomed!
160 |
161 | ### Dev Dependencies
162 |
163 | - [pre-commit](https://pre-commit.com/)
164 | - [bazelisk](https://github.com/bazelbuild/bazelisk)
165 |
166 | ### Commits
167 |
168 | I'm trying to follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
169 |
170 | ### Bootstraping
171 |
172 | ```sh
173 | pre-commit install
174 | pre-commit install --hook-type pre-push
175 | bazelisk sync
176 | ```
177 |
178 | ## License
179 |
180 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Fdanmx%2Fsigil?ref=badge_large)
181 |
182 | [Apache 2.0](LICENSE)
183 |
184 | ## Considerations
185 |
186 | *Sigil* was inspired by [xen0l's aws-gate](https://github.com/xen0l/aws-gate).
187 |
--------------------------------------------------------------------------------
/WORKSPACE:
--------------------------------------------------------------------------------
1 | # gazelle:repository_macro tools/repositories.bzl%go_repositories
2 |
3 | workspace(
4 | name = "com_github_danmx_sigil",
5 | )
6 |
7 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file")
8 |
9 | # Golang
10 | http_archive(
11 | name = "io_bazel_rules_go",
12 | sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f",
13 | urls = [
14 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip",
15 | "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip",
16 | ],
17 | )
18 |
19 | load("@io_bazel_rules_go//go:deps.bzl", "go_download_sdk", "go_register_toolchains", "go_rules_dependencies")
20 |
21 | go_download_sdk(
22 | name = "go_sdk",
23 | version = "1.17.2",
24 | )
25 |
26 | go_rules_dependencies()
27 |
28 | go_register_toolchains()
29 |
30 | # gazelle
31 | http_archive(
32 | name = "bazel_gazelle",
33 | sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb",
34 | urls = [
35 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
36 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz",
37 | ],
38 | )
39 |
40 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
41 |
42 | gazelle_dependencies()
43 |
44 | # Container image
45 | http_archive(
46 | name = "io_bazel_rules_docker",
47 | sha256 = "92779d3445e7bdc79b961030b996cb0c91820ade7ffa7edca69273f404b085d5",
48 | strip_prefix = "rules_docker-0.20.0",
49 | urls = ["https://github.com/bazelbuild/rules_docker/releases/download/v0.20.0/rules_docker-v0.20.0.tar.gz"],
50 | )
51 |
52 | load(
53 | "@io_bazel_rules_docker//repositories:repositories.bzl",
54 | container_repositories = "repositories",
55 | )
56 |
57 | container_repositories()
58 |
59 | load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps")
60 |
61 | container_deps()
62 |
63 | load(
64 | "@io_bazel_rules_docker//go:image.bzl",
65 | _go_image_repos = "repositories",
66 | )
67 |
68 | _go_image_repos()
69 |
70 | load("//tools:repositories.bzl", "go_repositories")
71 |
72 | go_repositories()
73 |
74 | # GoMock
75 | http_archive(
76 | name = "com_github_jmhodges_bazel_gomock",
77 | sha256 = "82a5fb946d2eb0fed80d3d70c2556784ec6cb5c35cd65a1b5e93e46f99681650",
78 | strip_prefix = "bazel_gomock-1.3",
79 | urls = ["https://github.com/jmhodges/bazel_gomock/archive/v1.3.tar.gz"],
80 | )
81 |
82 | # AWS Session Manager Plugin
83 | http_file(
84 | name = "session_manager_plugin_deb",
85 | downloaded_file_path = "session-manager-plugin.deb",
86 | sha256 = "f1c03d2aaad9f89f73fc70f1c1cdef0e2877a03b86cca3c8b5c97992c6344449",
87 | urls = ["https://s3.amazonaws.com/session-manager-downloads/plugin/1.2.245.0/ubuntu_64bit/session-manager-plugin.deb"],
88 | )
89 |
90 | # golangci-lint & git-chglog
91 | http_archive(
92 | name = "com_github_danmx_bazel_tools",
93 | sha256 = "822a9c9f04c02418d17efcd58dd37c4890f8eb77e645f15281d43b7bbd2d1637",
94 | strip_prefix = "bazel-tools-0.3.1",
95 | urls = ["https://github.com/danmx/bazel-tools/archive/0.3.1.tar.gz"],
96 | )
97 |
98 | load("@com_github_danmx_bazel_tools//git-chglog:deps.bzl", "git_chglog_dependencies")
99 | load("@com_github_danmx_bazel_tools//golangci-lint:deps.bzl", "golangci_lint_dependencies")
100 |
101 | git_chglog_dependencies()
102 |
103 | golangci_lint_dependencies()
104 |
--------------------------------------------------------------------------------
/cmd/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library")
2 |
3 | go_library(
4 | name = "go_default_library",
5 | srcs = [
6 | "list.go",
7 | "root.go",
8 | "session.go",
9 | "ssh.go",
10 | "verify.go",
11 | ],
12 | importpath = "github.com/danmx/sigil/cmd",
13 | visibility = ["//visibility:public"],
14 | deps = [
15 | "//pkg/aws:go_default_library",
16 | "//pkg/list:go_default_library",
17 | "//pkg/session:go_default_library",
18 | "//pkg/ssh:go_default_library",
19 | "@com_github_mitchellh_go_homedir//:go_default_library",
20 | "@com_github_sirupsen_logrus//:go_default_library",
21 | "@com_github_spf13_cobra//:go_default_library",
22 | "@com_github_spf13_viper//:go_default_library",
23 | ],
24 | )
25 |
--------------------------------------------------------------------------------
/cmd/list.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/danmx/sigil/pkg/aws"
8 | "github.com/danmx/sigil/pkg/list"
9 |
10 | log "github.com/sirupsen/logrus"
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | var (
15 | sessionFilters = map[string]string{
16 | "after": "",
17 | "before": "",
18 | "target": "",
19 | "owner": "",
20 | }
21 | // listCmd represents the list command
22 | listCmd = &cobra.Command{
23 | Use: "list [--type TYPE] ... { [--instance-ids IDs] [--instance-tags TAGS] | [--session-filters FILTERS] }",
24 | DisableFlagsInUseLine: true,
25 | Short: "List available EC2 instances or SSM sessions",
26 | Long: `Show list of all EC2 instances with AWS SSM Agent running or active SSM sessions.
27 |
28 | Supported groups of filters:
29 | - instances:
30 | - tags - list of tag keys with a list of values for given keys
31 | - ids - list of instastance ids
32 | - sessions:
33 | - after - the timestamp, in ISO-8601 Extended format, to see sessions that started after given date
34 | - before - the timestamp, in ISO-8601 Extended format, to see sessions that started before given date
35 | - target - an instance to which session connections have been made
36 | - owner - an AWS user account to see a list of sessions started by that user
37 |
38 | Filter format examples:
39 | [default.filters.session]
40 | after="2018-08-29T00:00:00Z"
41 | before="2019-08-29T00:00:00Z"
42 | target="i-xxxxxxxxxxxxxxxx1"
43 | owner="user@example.com"
44 | [default.filters.instance]
45 | ids=["i-xxxxxxxxxxxxxxxx1","i-xxxxxxxxxxxxxxxx2"]
46 | tags=[{key="Name",values=["WebApp1","WebApp2"]}]
47 | `,
48 | Aliases: []string{"ls", "l"},
49 | Example: fmt.Sprintf(`%s list --output-format wide --instance-tags '[{"key":"Name","values":["Web","DB"]}]'`, appName),
50 | //nolint:dupl // deduplicating it wouldn't provide much value
51 | PreRunE: func(cmd *cobra.Command, args []string) error {
52 | // Config bindings
53 | for flag, lookup := range map[string]string{
54 | "output-format": "output-format",
55 | "interactive": "interactive",
56 | "filters.session": "session-filters",
57 | "filters.instance.ids": "session-filters",
58 | "filters.instance.tags": "instance-tags",
59 | "list-type": "type",
60 | } {
61 | if err := cfg.BindPFlag(flag, cmd.Flags().Lookup(lookup)); err != nil {
62 | log.WithFields(log.Fields{
63 | "flag": flag,
64 | "lookup": lookup,
65 | }).Error(err)
66 | return err
67 | }
68 | }
69 | // returns err
70 | return aws.VerifyDependencies()
71 | },
72 | RunE: func(cmd *cobra.Command, args []string) error {
73 | var filters aws.Filters
74 | if err := cfg.UnmarshalKey("filters", &filters); err != nil {
75 | log.Error("failed unmarshaling filters")
76 | return fmt.Errorf("failed unmarshaling filters: %s", err)
77 | }
78 | outputFormat := cfg.GetString("output-format")
79 | profile := cfg.GetString("profile")
80 | region := cfg.GetString("region")
81 | interactive := cfg.GetBool("interactive")
82 | listType := cfg.GetString("list-type")
83 | instanceIDs := cfg.GetStringSlice("filters.instance.ids")
84 | mfaToken := cfg.GetString("mfa")
85 | trace := log.IsLevelEnabled(log.TraceLevel)
86 | // hack to get map[string]string from args
87 | // https://github.com/spf13/viper/issues/608
88 | if cmd.Flags().Changed("session-filters") {
89 | filters.Session = aws.SessionFilters{
90 | After: sessionFilters["after"],
91 | Before: sessionFilters["before"],
92 | Target: sessionFilters["target"],
93 | Owner: sessionFilters["owner"],
94 | }
95 | }
96 | if cmd.Flags().Changed("instance-ids") {
97 | filters.Instance.IDs = instanceIDs
98 | }
99 | var tags []aws.TagValues
100 | if cmd.Flags().Changed("instance-tags") {
101 | if err := json.Unmarshal([]byte(cfg.GetString("filters.instance.tags")), &tags); err != nil {
102 | log.WithField("tags", cfg.GetString("filters.instance.tags")).Error("failed unmarshaling tags")
103 | return fmt.Errorf("failed unmarshaling tags: %s", err)
104 | }
105 | filters.Instance.Tags = tags
106 | }
107 | log.WithFields(log.Fields{
108 | "filters": filters,
109 | "output-format": outputFormat,
110 | "region": region,
111 | "profile": profile,
112 | "mfa": mfaToken,
113 | "interactive": interactive,
114 | "type": listType,
115 | "instanceIDs": instanceIDs,
116 | "sessionFilters": sessionFilters,
117 | "tags": tags,
118 | "trace": trace,
119 | }).Debug("List inputs")
120 | input := &list.StartInput{
121 | OutputFormat: &outputFormat,
122 | MFAToken: &mfaToken,
123 | Region: ®ion,
124 | Profile: &profile,
125 | Filters: &filters,
126 | Interactive: &interactive,
127 | Type: &listType,
128 | Trace: &trace,
129 | }
130 | err := list.Start(input)
131 | if err != nil {
132 | log.Error(err)
133 | return err
134 | }
135 | return nil
136 | },
137 | DisableAutoGenTag: true,
138 | }
139 | )
140 |
141 | func init() {
142 | rootCmd.AddCommand(listCmd)
143 |
144 | listCmd.Flags().String("output-format", list.FormatText, fmt.Sprintf("specify output format: %s/%s/%s/%s", list.FormatText, list.FormatWide, list.FormatJSON, list.FormatYAML))
145 | listCmd.Flags().BoolP("interactive", "i", false, "pick an instance or a session from a list and start or terminate the session")
146 | listCmd.Flags().StringP("type", "t", list.TypeListInstances, fmt.Sprintf("specify list type: %s/%s", list.TypeListInstances, list.TypeListSessions))
147 | listCmd.Flags().StringToStringVar(&sessionFilters, "session-filters", sessionFilters, "specify session filters to limit results")
148 | listCmd.Flags().StringSlice("instance-ids", []string{}, "specify instance ids to limit results")
149 | listCmd.Flags().String("instance-tags", "", "specify instance tags, in JSON format, to limit results")
150 | }
151 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path"
7 | "strings"
8 |
9 | "github.com/danmx/sigil/pkg/aws"
10 |
11 | homedir "github.com/mitchellh/go-homedir"
12 | log "github.com/sirupsen/logrus"
13 | "github.com/spf13/cobra"
14 | "github.com/spf13/viper"
15 | )
16 |
17 | var (
18 | appName string = "sigil"
19 | // appVersion is the semantic appVersion (added at compile time)
20 | appVersion string
21 | // gitCommit is the git commit id (added at compile time)
22 | gitCommit string
23 | // logLevel level is setting loging level (added at compile time)
24 | logLevel string = "panic"
25 | // dev is turning a debug mode (added at compile time)
26 | dev string = "false"
27 |
28 | workDir string
29 | cfg *viper.Viper
30 |
31 | cfgFileName = "config"
32 | cfgType = "toml"
33 | workDirName = "." + appName
34 |
35 | // rootCmd represents the base command when called without any subcommands
36 | rootCmd = &cobra.Command{
37 | Use: appName,
38 | Short: "AWS SSM Session manager client",
39 | Long: `A tool for establishing a session in EC2 instances with AWS SSM Agent installed`,
40 | Version: fmt.Sprintf("%s (build %s)", appVersion, gitCommit),
41 | DisableAutoGenTag: true,
42 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
43 | // returns err
44 | return aws.AppendUserAgent(appName + "/" + appVersion)
45 | },
46 | }
47 | )
48 |
49 | // Execute adds all child commands to the root command and sets flags appropriately.
50 | // This is called by main.main(). It only needs to happen once to the rootCmd.
51 | func Execute() error {
52 | // returns err
53 | return rootCmd.Execute()
54 | }
55 |
56 | func init() {
57 | // Set debug
58 | if dev == "true" {
59 | log.SetReportCaller(true)
60 | }
61 | // Set startup Log level
62 | if err := setLogLevel(logLevel); err != nil {
63 | log.WithFields(log.Fields{
64 | "logLevel": logLevel,
65 | }).Fatal(err)
66 | }
67 | // Find home directory.
68 | home, err := homedir.Dir()
69 | if err != nil {
70 | fmt.Fprintln(os.Stderr, err)
71 | log.Fatal(err)
72 | }
73 | workDir = path.Join(home, workDirName)
74 | stat, err := os.Stat(workDir)
75 | if !(err == nil && stat.IsDir()) {
76 | if err := os.MkdirAll(workDir, 0750); err != nil { //nolint:gomnd // Linux file permissions
77 | fmt.Fprintln(os.Stderr, err)
78 | log.Fatal(err)
79 | }
80 | }
81 |
82 | // init config and env vars
83 | cobra.OnInitialize(func() {
84 | if err := initConfig(rootCmd); err != nil {
85 | fmt.Fprintln(os.Stderr, err)
86 | log.Fatal(err)
87 | }
88 | })
89 |
90 | // Config file
91 | rootCmd.PersistentFlags().StringP("config", "c", "", "full config file path, supported formats: json/yaml/toml/hcl/props")
92 | rootCmd.PersistentFlags().StringP("config-profile", "p", "default", "pick the config profile")
93 | // Log level
94 | rootCmd.PersistentFlags().String("log-level", logLevel, "specify the log level: trace/debug/info/warn/error/fatal/panic")
95 | // AWS
96 | rootCmd.PersistentFlags().StringP("region", "r", "", "specify AWS region")
97 | rootCmd.PersistentFlags().String("profile", "", "specify AWS profile")
98 | rootCmd.PersistentFlags().StringP("mfa", "m", "", "specify MFA token")
99 | }
100 |
101 | // initConfig reads in config file and ENV variables if set
102 | func initConfig(cmd *cobra.Command) error {
103 | cfg = viper.New()
104 | // Environment variables
105 | cfg.SetEnvPrefix(appName)
106 | cfg.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
107 | // Root config bindings
108 | for _, key := range []string{"config", "config-profile", "log-level"} {
109 | if err := cfg.BindEnv(key); err != nil {
110 | log.WithFields(log.Fields{
111 | "env": key,
112 | }).Error(err)
113 | return err
114 | }
115 | if err := cfg.BindPFlag(key, cmd.PersistentFlags().Lookup(key)); err != nil {
116 | log.WithFields(log.Fields{
117 | "flag": key,
118 | }).Error(err)
119 | return err
120 | }
121 | }
122 | cfgFile := cfg.GetString("config")
123 | cfgProfile := cfg.GetString("config-profile")
124 | logLevel := cfg.GetString("log-level")
125 |
126 | // Set Log level
127 | if err := setLogLevel(logLevel); err != nil {
128 | fmt.Fprintln(os.Stderr, err)
129 | log.WithFields(log.Fields{
130 | "logLevel": logLevel,
131 | }).Error(err)
132 | return err
133 | }
134 | if cfgFile != "" {
135 | // Use config file from the flag.
136 | cfg.SetConfigFile(cfgFile)
137 | } else {
138 | // Search config in home directory with name from cfgFileName (without extension).
139 | cfg.AddConfigPath(workDir)
140 | cfg.SetConfigName(cfgFileName)
141 | cfg.SetConfigType(cfgType)
142 | }
143 |
144 | // If a config file is found, read it in.
145 | if err := cfg.ReadInConfig(); err == nil {
146 | log.WithFields(log.Fields{
147 | "config": cfg.ConfigFileUsed(),
148 | }).Debug("Using config file")
149 | cfg, err = safeSub(cfg, cfgProfile)
150 | if err != nil {
151 | fmt.Fprintln(os.Stderr, err)
152 | log.WithFields(log.Fields{
153 | "config-profile": cfgProfile,
154 | }).Error(err)
155 | return err
156 | }
157 | }
158 |
159 | // Rebinding config bindings that will be propagated to subcommands because of the subconfig (config profile)
160 | cfg.SetEnvPrefix(appName)
161 | if err := cfg.BindEnv("mfa"); err != nil {
162 | log.WithFields(log.Fields{
163 | "env": "mfa",
164 | }).Error(err)
165 | return err
166 | }
167 | for _, key := range []string{"region", "config-profile", "profile"} {
168 | if err := cfg.BindPFlag(key, cmd.PersistentFlags().Lookup(key)); err != nil {
169 | log.WithFields(log.Fields{
170 | "flag": key,
171 | }).Error(err)
172 | return err
173 | }
174 | }
175 | return nil
176 | }
177 |
178 | // because of https://github.com/spf13/viper/issues/616
179 | func safeSub(v *viper.Viper, profile string) (*viper.Viper, error) {
180 | subConfig := v.Sub(profile)
181 | if subConfig == nil {
182 | return nil, fmt.Errorf("config profile doesn't exist. Profile: %s", profile)
183 | }
184 | return subConfig, nil
185 | }
186 |
187 | // setLogLevel sets the log level
188 | func setLogLevel(level string) error {
189 | // Log level
190 | switch level {
191 | case "error":
192 | log.SetLevel(log.ErrorLevel)
193 | case "debug":
194 | log.SetLevel(log.DebugLevel)
195 | case "info":
196 | log.SetLevel(log.InfoLevel)
197 | case "warn":
198 | log.SetLevel(log.WarnLevel)
199 | case "fatal":
200 | log.SetLevel(log.FatalLevel)
201 | case "panic":
202 | log.SetLevel(log.PanicLevel)
203 | case "trace":
204 | log.SetLevel(log.TraceLevel)
205 | default:
206 | return fmt.Errorf("unsupported log level: %s", level)
207 | }
208 | return nil
209 | }
210 |
--------------------------------------------------------------------------------
/cmd/session.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/danmx/sigil/pkg/aws"
7 | "github.com/danmx/sigil/pkg/session"
8 |
9 | log "github.com/sirupsen/logrus"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | // sessionCmd represents the session command
14 | var sessionCmd = &cobra.Command{
15 | Use: "session [--type TYPE] ... TARGET",
16 | DisableFlagsInUseLine: true,
17 | Short: "Start a session",
18 | Long: `Start a new session in chosen EC2 instance.`,
19 | Aliases: []string{"sess", "s"},
20 | Example: fmt.Sprintf("%s session --type instance-id i-xxxxxxxxxxxxxxxxx", appName),
21 | Args: cobra.MaximumNArgs(1),
22 | PreRunE: func(cmd *cobra.Command, args []string) error {
23 | if len(args) != 0 && cmd.Flags().Lookup("target").Changed {
24 | return fmt.Errorf("can't use both target argument (%s) and deprecated flag (%s)", args[0], cmd.Flags().Lookup("target").Value.String())
25 | }
26 | // Config bindings
27 | for _, flag := range []string{"target", "type"} {
28 | if err := cfg.BindPFlag(flag, cmd.Flags().Lookup(flag)); err != nil {
29 | log.WithFields(log.Fields{
30 | "flag": flag,
31 | }).Error(err)
32 | return err
33 | }
34 | }
35 | if len(args) > 0 {
36 | cfg.Set("target", args[0])
37 | }
38 | // returns err
39 | return aws.VerifyDependencies()
40 | },
41 | RunE: func(cmd *cobra.Command, args []string) error {
42 | target := cfg.GetString("target")
43 | targetType := cfg.GetString("type")
44 | profile := cfg.GetString("profile")
45 | region := cfg.GetString("region")
46 | mfaToken := cfg.GetString("mfa")
47 | trace := log.IsLevelEnabled(log.TraceLevel)
48 | log.WithFields(log.Fields{
49 | "target": target,
50 | "type": targetType,
51 | "region": region,
52 | "profile": profile,
53 | "mfa": mfaToken,
54 | "trace": trace,
55 | }).Debug("Session inputs")
56 | input := &session.StartInput{
57 | Target: &target,
58 | TargetType: &targetType,
59 | Region: ®ion,
60 | Profile: &profile,
61 | MFAToken: &mfaToken,
62 | Trace: &trace,
63 | }
64 | // returns err
65 | return session.Start(input)
66 | },
67 | DisableAutoGenTag: true,
68 | }
69 |
70 | func init() {
71 | rootCmd.AddCommand(sessionCmd)
72 |
73 | sessionCmd.Flags().String("target", "", "specify the target depending on the type")
74 | // Deprecating the target flag
75 | err := sessionCmd.Flags().MarkDeprecated("target", "this flag will be deprecated in future releases, use args instead")
76 | if err != nil {
77 | log.WithField("flag", sessionCmd.Flags().Lookup("target")).Error(err)
78 | }
79 | sessionCmd.Flags().String("type", aws.TargetTypeInstanceID, fmt.Sprintf("specify target type: %s/%s/%s", aws.TargetTypeInstanceID, aws.TargetTypePrivateDNS, aws.TargetTypeName))
80 | }
81 |
--------------------------------------------------------------------------------
/cmd/ssh.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path"
7 |
8 | "github.com/danmx/sigil/pkg/aws"
9 | "github.com/danmx/sigil/pkg/ssh"
10 |
11 | log "github.com/sirupsen/logrus"
12 | "github.com/spf13/cobra"
13 | )
14 |
15 | const tempKeyName = "temp_key"
16 |
17 | var (
18 | portNum uint64 = 22
19 | sshCmd = &cobra.Command{
20 | Use: "ssh [--type TYPE] ... [ { --gen-key-pair [--gen-key-dir DIR] | --pub-key PUB_KEY_PATH } ] TARGET",
21 | DisableFlagsInUseLine: true,
22 | Short: "Start ssh session",
23 | Long: `Start a new ssh for chosen EC2 instance.`,
24 | Args: cobra.MaximumNArgs(1),
25 | //nolint:dupl // deduplicating it wouldn't provide much value
26 | PreRunE: func(cmd *cobra.Command, args []string) error {
27 | if len(args) != 0 && cmd.Flags().Lookup("target").Changed {
28 | return fmt.Errorf("can't use both target argument (%s) and deprecated flag (%s)", args[0], cmd.Flags().Lookup("target").Value.String())
29 | }
30 | // Config bindings
31 | for flag, lookup := range map[string]string{
32 | "target": "target",
33 | "type": "type",
34 | "pub-key": "pub-key",
35 | "os-user": "os-user",
36 | "gen-key-pair": "gen-key-pair",
37 | "gen-key-dir": "gen-key-dir",
38 | } {
39 | if err := cfg.BindPFlag(flag, cmd.Flags().Lookup(lookup)); err != nil {
40 | log.WithFields(log.Fields{
41 | "flag": flag,
42 | "lookup": lookup,
43 | }).Error(err)
44 | return err
45 | }
46 | }
47 | if len(args) > 0 {
48 | cfg.Set("target", args[0])
49 | }
50 | // returns err
51 | return aws.VerifyDependencies()
52 | },
53 | RunE: func(cmd *cobra.Command, args []string) error {
54 | target := cfg.GetString("target")
55 | targetType := cfg.GetString("type")
56 | profile := cfg.GetString("profile")
57 | region := cfg.GetString("region")
58 | pubKey := cfg.GetString("pub-key")
59 | OSUser := cfg.GetString("os-user")
60 | genKeyPair := cfg.GetBool("gen-key-pair")
61 | genKeyDir := cfg.GetString("gen-key-dir")
62 | mfaToken := cfg.GetString("mfa")
63 | trace := log.IsLevelEnabled(log.TraceLevel)
64 | if genKeyPair {
65 | stat, err := os.Stat(genKeyDir)
66 | if !(err == nil && stat.IsDir()) {
67 | if err = os.MkdirAll(genKeyDir, 0750); err != nil { //nolint:gomnd // Linux file permissions
68 | return err
69 | }
70 | }
71 | if err != nil {
72 | err = fmt.Errorf("failed creating directory for temporary keys: %e", err)
73 | log.WithFields(log.Fields{
74 | "genKeyDir": genKeyDir,
75 | }).Error(err)
76 | return err
77 | }
78 | pubKey = path.Join(genKeyDir, tempKeyName+".pub")
79 | }
80 | log.WithFields(log.Fields{
81 | "target": target,
82 | "type": targetType,
83 | "region": region,
84 | "profile": profile,
85 | "mfa": mfaToken,
86 | "pub-key": pubKey,
87 | "port": portNum,
88 | "os-user": OSUser,
89 | "gen-key-pair": genKeyPair,
90 | "gen-key-dir": genKeyDir,
91 | "trace": trace,
92 | }).Debug("ssh inputs")
93 | input := &ssh.StartInput{
94 | Target: &target,
95 | TargetType: &targetType,
96 | PortNumber: &portNum,
97 | PublicKey: &pubKey,
98 | OSUser: &OSUser,
99 | GenKeyPair: &genKeyPair,
100 | Region: ®ion,
101 | Profile: &profile,
102 | MFAToken: &mfaToken,
103 | Trace: &trace,
104 | }
105 | // returns err
106 | return ssh.Start(input)
107 | },
108 | DisableAutoGenTag: true,
109 | }
110 | )
111 |
112 | func init() {
113 | rootCmd.AddCommand(sshCmd)
114 |
115 | sshCmd.Flags().String("target", "", "specify the target depending on the type")
116 | // Deprecating the target flag
117 | err := sshCmd.Flags().MarkDeprecated("target", "this flag will be deprecated in future releases, use args instead")
118 | if err != nil {
119 | log.WithField("flag", sshCmd.Flags().Lookup("target")).Error(err)
120 | }
121 | sshCmd.Flags().String("type", aws.TargetTypeInstanceID, fmt.Sprintf("specify target type: %s/%s/%s", aws.TargetTypeInstanceID, aws.TargetTypePrivateDNS, aws.TargetTypeName))
122 | sshCmd.Flags().Bool("gen-key-pair", false, fmt.Sprintf("generate a temporary key pair that will be send and used. By default use %s as an identity file", path.Join(workDir, tempKeyName)))
123 | sshCmd.Flags().String("gen-key-dir", workDir, "the directory where temporary keys will be generated")
124 | sshCmd.Flags().String("os-user", "ec2-user", "specify an instance OS user which will be using sent public key")
125 | sshCmd.Flags().String("pub-key", "", "local public key that will be send to the instance, ignored when gen-key-pair is true")
126 | sshCmd.Flags().Uint64Var(&portNum, "port", portNum, "specify ssh port")
127 | }
128 |
--------------------------------------------------------------------------------
/cmd/verify.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/danmx/sigil/pkg/aws"
7 |
8 | "github.com/spf13/cobra"
9 | )
10 |
11 | // verifyCmd represents the verify command
12 | var verifyCmd = &cobra.Command{
13 | Use: "verify",
14 | Short: "Verify if all external dependencies are available",
15 | Long: `This command will check if all dependecies are installed.
16 | Plugin documentation: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html`,
17 | RunE: func(cmd *cobra.Command, args []string) error {
18 | if err := aws.VerifyDependencies(); err != nil {
19 | return err
20 | }
21 | fmt.Print("All dependencies are installed\n")
22 | return nil
23 | },
24 | TraverseChildren: false,
25 | DisableFlagsInUseLine: true,
26 | DisableAutoGenTag: true,
27 | }
28 |
29 | func init() {
30 | rootCmd.AddCommand(verifyCmd)
31 | }
32 |
--------------------------------------------------------------------------------
/docs/00-get-started.md:
--------------------------------------------------------------------------------
1 | # Get Started
2 |
3 | ## External dependencies
4 |
5 | To start using `sigil` you need to make sure you have all the necessary dependencies.
6 |
7 | ### Local
8 |
9 | - AWS [session-manager-plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html) (version 1.1.17.0+ for SSH support)
10 |
11 | ### Remote
12 |
13 | - target EC2 instance must have AWS SSM Agent installed ([full guide](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html)) (version 2.3.672.0+ for SSH support)
14 | - AWS [ec2-instance-connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-connect-set-up.html) to use SSH with your own and/or temporary keys
15 | - target EC2 instance profile should have **AmazonSSMManagedInstanceCore** managed IAM policy attached or a specific policy with similar permissions (check [About Policies for a Systems Manager Instance Profile](https://docs.aws.amazon.com/systems-manager/latest/userguide/setup-instance-profile.html) and [About Minimum S3 Bucket Permissions for SSM Agent](https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-minimum-s3-permissions.html))
16 |
17 | ## Download
18 |
19 | To download `sigil` you can use:
20 |
21 | ### Homebrew
22 |
23 | ```shell
24 | brew tap danmx/sigil
25 | brew install sigil
26 | ```
27 |
28 | or
29 |
30 | ```shell
31 | brew install danmx/sigil/sigil
32 | ```
33 |
34 | ### Docker
35 |
36 | ```shell
37 | docker pull danmx/sigil:0.7
38 | ```
39 |
40 | ### Source code
41 |
42 | Pull the repository and build binaries.
43 |
44 | ```shell
45 | git clone https://github.com/danmx/sigil.git
46 | cd sigil
47 | bazelisk sync
48 | ```
49 |
50 | For all binaries (`development` and `release`) and Docker image run:
51 |
52 | ```shell
53 | bazelisk build //...
54 | ```
55 |
56 | To build a specific platform (Linux, Mac, Windows) use:
57 |
58 | ```shell
59 | bazelisk build --config cross:[darwin|linux|windows]_amd64 :[dev|release]
60 | ```
61 |
62 | for working Docker image:
63 |
64 | ```shell
65 | bazelisk build --config cross:linux_amd64 :[dev|release]-image
66 | ```
67 |
68 | To debug the image locally use `run` instead of `build`.
69 |
--------------------------------------------------------------------------------
/docs/10-configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | The configuration varies depending on the command. For more defails (like default values) check [usage](usage/README.md) section or [man](man/sigil.md) pages.
4 |
5 | ## Global flags and corresponding environment variables
6 |
7 | ENV variables are case sensitive.
8 |
9 | | Flag | Environment variable | Description |
10 | | ----------------------- | :--------------------------------------------------------------------------------------------: | :----------------------------------------------------------------- |
11 | | `-c`/`--config` | `SIGIL_CONFIG` | full config file path, supported formats: json/yaml/toml/hcl/props |
12 | | `-p`/`--config-profile` | `SIGIL_CONFIG_PROFILE` | pick the config profile |
13 | | `--log-level` | `SIGIL_LOG_LEVEL` | specify the log level: trace/debug/info/warn/error/fatal/panic |
14 | | `-m`/`--mfa` | `SIGIL_MFA` | specify MFA token |
15 | | `--profile` | [`AWS_PROFILE`/`AWS_DEFAULT_PROFILE`](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) | specify AWS profile |
16 | | `-r`/`--region` | [`AWS_REGION`/`AWS_DEFAULT_REGION`](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) | specify AWS region |
17 |
18 | ## Config file
19 |
20 | Description of different values of the configuration file.
21 |
22 | | Parameter | Command(s) | Description |
23 | | ------------------------ | :-------------: | :--------------------------------------------------------------------------- |
24 | | `profile` | **all** | specify AWS profile |
25 | | `type` | `session`/`ssh` | specify target type |
26 | | `target` | `session`/`ssh` | specify the target depending on the type |
27 | | `os-user` | `ssh` | specify an instance OS user which will be using sent public key |
28 | | `port` | `ssh` | specify ssh port |
29 | | `gen-key-pair` | `ssh` | generate a temporary key pair that will be send and used |
30 | | `gen-key-dir` | `ssh` | the directory where temporary keys will be generated |
31 | | `pub-key` | `ssh` | local public key that will be send to the instance |
32 | | `output-format` | `list` | specify output format |
33 | | `interactive` | `list` | pick an instance or a session from a list and start or terminate the session |
34 | | `filters.session.after` | `list` | show only sessions that started after given datetime |
35 | | `filters.session.before` | `list` | show only sessions that started before given datetime |
36 | | `filters.session.target` | `list` | show only sessions for given target |
37 | | `filters.session.owner` | `list` | show only sessions owned by given owner |
38 | | `filters.instance.ids` | `list` | show only instances with matching IDs |
39 | | `filters.instance.tags` | `list` | show only instances with matching tags |
40 |
41 | ## Example
42 |
43 | An example of a fully configured `default` profile.
44 |
45 | ```toml
46 | [default]
47 | type = "name"
48 | target = "Worker"
49 | type = "instance-id"
50 | output-format = "text"
51 | region = "eu-west-1"
52 | profile = "dev"
53 | interactive = false
54 | os-user = "ec2-user"
55 | gen-key-pair = false
56 | gen-key-dir = "/tmp/sigil"
57 | pub-key = "~/.ssh/dev.pub"
58 | [default.filters.session]
59 | after="2018-08-29T00:00:00Z"
60 | before="2019-08-29T00:00:00Z"
61 | target="i-xxxxxxxxxxxxxxxx1"
62 | owner="user@example.com"
63 | [default.filters.instance]
64 | ids=["i-xxxxxxxxxxxxxxxx1","i-xxxxxxxxxxxxxxxx2"]
65 | tags = [
66 | {key="Name", values=["Web","DB"] }
67 | ]
68 | ```
69 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | - [get started](00-get-started.md)
4 | - [configuration](10-configuration.md)
5 | - [usage](usage/README.md)
6 |
--------------------------------------------------------------------------------
/docs/usage/00-list.md:
--------------------------------------------------------------------------------
1 | # List
2 |
3 | This command list all active instances and sessions that match the filter.
4 | When interactive mode is enabled you can start a session in a given instance or terminate active session.
5 |
6 | ```console
7 | sigil list [--type TYPE] ... { [--instance-ids IDs] [--instance-tags TAGS] | [--session-filters FILTERS] }
8 | ```
9 |
10 | [Man](../man/sigil_list.md) page
11 |
12 | ## Sample config
13 |
14 | Config file settings that affect the command
15 |
16 | ```toml
17 | [default]
18 | output-format = "text"
19 | region = "eu-west-1"
20 | profile = "dev"
21 | interactive = false
22 | [default.filters.session]
23 | after="2018-08-29T00:00:00Z"
24 | before="2019-08-29T00:00:00Z"
25 | target="i-xxxxxxxxxxxxxxxx1"
26 | owner="user@example.com"
27 | [default.filters.instance]
28 | ids=["i-xxxxxxxxxxxxxxxx1","i-xxxxxxxxxxxxxxxx2"]
29 | tags = [
30 | {key="Name", values=["Web","DB"] }
31 | ]
32 | ```
33 |
34 | ## Examples
35 |
36 | List instances
37 |
38 | ```console
39 | $ sigil list --instance-tags '[{"key":"Name","values":["Web","DB"]}]'
40 | Index Name Instance ID IP Address Private DNS Name
41 | 1 Web i-xxxxxxxxxxxxxxxx1 10.10.10.1 test1.local
42 | 2 DB i-xxxxxxxxxxxxxxxx2 10.10.10.2 test2.local
43 | ```
44 |
45 | List sessions
46 |
47 | ```console
48 | $ sigil list -t sessions'
49 | Index Session ID Target Start Date
50 | 1 test-1234567890 i-xxxxxxxxxxxxxxxx1 2019-05-03T10:08:44Z
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/usage/10-session.md:
--------------------------------------------------------------------------------
1 | # Session
2 |
3 | Start a new session in chosen EC2 instance based on its instance ID, name tag, or private DNS name.
4 |
5 | ```console
6 | sigil session [--type TYPE] ... TARGET
7 | ```
8 |
9 | [Man](../man/sigil_session.md) page
10 |
11 | ## Sample config
12 |
13 | Config file settings that affect the command
14 |
15 | ```toml
16 | [default]
17 | type = "name"
18 | target = "Worker"
19 | region = "eu-west-1"
20 | profile = "dev"
21 | ```
22 |
23 | ## Examples
24 |
25 | ```console
26 | $ sigil -r eu-west-1 session --type instance-id i-xxxxxxxxxxxxxxxxx
27 | Starting session with SessionId: example
28 | sh-4.2$
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/usage/20-ssh.md:
--------------------------------------------------------------------------------
1 | # SSH
2 |
3 | Start a new ssh for chosen EC2 instance based on its instance ID, name tag, or private DNS name.
4 |
5 | ```console
6 | ssh [--type TYPE] ... [ { --gen-key-pair [--gen-key-dir DIR] | --pub-key PUB_KEY_PATH } ] TARGET
7 | ```
8 |
9 | [Man](../man/sigil_ssh.md) page
10 |
11 | ## Sample config
12 |
13 | Config file settings that affect the command
14 |
15 | ```toml
16 | [default]
17 | type = "name"
18 | target = "Worker"
19 | region = "eu-west-1"
20 | profile = "dev"
21 | os-user = "ec2-user"
22 | gen-key-pair = false
23 | pub-key = "~/.ssh/dev.pub"
24 | ```
25 |
26 | ## Examples
27 |
28 | `ssh_config` config file example:
29 |
30 | ```ssh_config
31 | Host i-* mi-*
32 | IdentityFile /tmp/sigil/%h/temp_key
33 | IdentitiesOnly yes
34 | ProxyCommand sigil ssh --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h
35 | Host *.compute.internal
36 | IdentityFile /tmp/sigil/%h/temp_key
37 | IdentitiesOnly yes
38 | ProxyCommand sigil ssh --type private-dns --port %p --pub-key /tmp/sigil/%h/temp_key.pub --gen-key-pair --os-user %r --gen-key-dir /tmp/sigil/%h/ %h
39 | ```
40 |
41 | ```console
42 | $ ssh ec2-user@ip-10-0-0-5.eu-west-1.compute.internal
43 | Last login: Tue Jun 18 20:50:59 2019 from 10.0.0.5
44 | ...
45 | [ec2-user@example ~]$
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/usage/30-verify.md:
--------------------------------------------------------------------------------
1 | # Verify
2 |
3 | Check if all local dependencies are available.
4 |
5 | ```console
6 | sigil verify
7 | ```
8 |
9 | [Man](../man/sigil_verify.md) page
10 |
11 | ## Examples
12 |
13 | Verify dependencies
14 |
15 | ```console
16 | $ sigil verify
17 | All dependencies are installed
18 | ```
19 |
--------------------------------------------------------------------------------
/docs/usage/README.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | List of available commands and their example usage:
4 |
5 | - [list](00-list.md)
6 | - [session](10-session.md)
7 | - [ssh](20-ssh.md)
8 | - [verify](30-verify.md)
9 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/danmx/sigil
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/aws/aws-sdk-go v1.41.14
7 | github.com/golang/mock v1.6.0
8 | github.com/mitchellh/go-homedir v1.1.0
9 | github.com/sirupsen/logrus v1.8.1
10 | github.com/spf13/cobra v1.2.1
11 | github.com/spf13/viper v1.9.0
12 | github.com/stretchr/testify v1.7.0
13 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
14 | gopkg.in/yaml.v2 v2.4.0
15 | )
16 |
17 | require (
18 | github.com/davecgh/go-spew v1.1.1 // indirect
19 | github.com/fsnotify/fsnotify v1.5.1 // indirect
20 | github.com/hashicorp/hcl v1.0.0 // indirect
21 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
22 | github.com/jmespath/go-jmespath v0.4.0 // indirect
23 | github.com/magiconair/properties v1.8.5 // indirect
24 | github.com/mitchellh/mapstructure v1.4.2 // indirect
25 | github.com/pelletier/go-toml v1.9.4 // indirect
26 | github.com/pmezard/go-difflib v1.0.0 // indirect
27 | github.com/spf13/afero v1.6.0 // indirect
28 | github.com/spf13/cast v1.4.1 // indirect
29 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
30 | github.com/spf13/pflag v1.0.5 // indirect
31 | github.com/subosito/gotenv v1.2.0 // indirect
32 | golang.org/x/sys v0.0.0-20211031064116-611d5d643895 // indirect
33 | golang.org/x/text v0.3.7 // indirect
34 | gopkg.in/ini.v1 v1.63.2 // indirect
35 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
36 | )
37 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/danmx/sigil/cmd"
7 | )
8 |
9 | func main() {
10 | if err := cmd.Execute(); err != nil {
11 | os.Exit(1) //nolint:gomnd // custom error codes wouldn't provide much value
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/aws/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock")
3 |
4 | go_library(
5 | name = "go_default_library",
6 | srcs = [
7 | "aws.go",
8 | "interface.go",
9 | "list.go",
10 | "session.go",
11 | "ssh.go",
12 | ],
13 | importpath = "github.com/danmx/sigil/pkg/aws",
14 | visibility = ["//visibility:public"],
15 | deps = [
16 | "//pkg/aws/helpers:go_default_library",
17 | "//pkg/aws/log:go_default_library",
18 | "//pkg/os:go_default_library",
19 | "@com_github_aws_aws_sdk_go//aws:go_default_library",
20 | "@com_github_aws_aws_sdk_go//aws/client:go_default_library",
21 | "@com_github_aws_aws_sdk_go//aws/credentials/stscreds:go_default_library",
22 | "@com_github_aws_aws_sdk_go//aws/session:go_default_library",
23 | "@com_github_aws_aws_sdk_go//service/ec2:go_default_library",
24 | "@com_github_aws_aws_sdk_go//service/ec2/ec2iface:go_default_library",
25 | "@com_github_aws_aws_sdk_go//service/ec2instanceconnect:go_default_library",
26 | "@com_github_aws_aws_sdk_go//service/ec2instanceconnect/ec2instanceconnectiface:go_default_library",
27 | "@com_github_aws_aws_sdk_go//service/ssm:go_default_library",
28 | "@com_github_aws_aws_sdk_go//service/ssm/ssmiface:go_default_library",
29 | "@com_github_sirupsen_logrus//:go_default_library",
30 | ],
31 | )
32 |
33 | go_test(
34 | name = "go_default_test",
35 | srcs = [
36 | "aws_test.go",
37 | "ec2_aws_mock_test.go",
38 | "ec2instanceconnect_aws_mock_test.go",
39 | "helpers_mock_test.go",
40 | "list_test.go",
41 | "session_test.go",
42 | "ssh_test.go",
43 | "ssm_aws_mock_test.go",
44 | ],
45 | embed = [":go_default_library"],
46 | deps = [
47 | "@com_github_aws_aws_sdk_go//aws:go_default_library",
48 | "@com_github_aws_aws_sdk_go//aws/request:go_default_library",
49 | "@com_github_aws_aws_sdk_go//service/ec2:go_default_library",
50 | "@com_github_aws_aws_sdk_go//service/ec2instanceconnect:go_default_library",
51 | "@com_github_aws_aws_sdk_go//service/ssm:go_default_library",
52 | "@com_github_golang_mock//gomock:go_default_library",
53 | "@com_github_stretchr_testify//assert:go_default_library",
54 | ],
55 | )
56 |
57 | gomock(
58 | name = "mock_helpers",
59 | out = "helpers_mock_test.go",
60 | interfaces = [
61 | "OSExecIface",
62 | "OSIface",
63 | ],
64 | library = "//pkg/aws/helpers:go_default_library",
65 | package = "aws",
66 | self_package = "github.com/danmx/sigil/pkg/aws",
67 | )
68 |
69 | gomock(
70 | name = "mock_aws_ec2",
71 | out = "ec2_aws_mock_test.go",
72 | interfaces = [
73 | "EC2API",
74 | ],
75 | library = "@com_github_aws_aws_sdk_go//service/ec2/ec2iface:go_default_library",
76 | package = "aws",
77 | self_package = "github.com/danmx/sigil/pkg/aws",
78 | )
79 |
80 | gomock(
81 | name = "mock_aws_ssm",
82 | out = "ssm_aws_mock_test.go",
83 | interfaces = [
84 | "SSMAPI",
85 | ],
86 | library = "@com_github_aws_aws_sdk_go//service/ssm/ssmiface:go_default_library",
87 | package = "aws",
88 | self_package = "github.com/danmx/sigil/pkg/aws",
89 | )
90 |
91 | gomock(
92 | name = "mock_aws_ec2instanceconnect",
93 | out = "ec2instanceconnect_aws_mock_test.go",
94 | interfaces = [
95 | "EC2InstanceConnectAPI",
96 | ],
97 | library = "@com_github_aws_aws_sdk_go//service/ec2instanceconnect/ec2instanceconnectiface:go_default_library",
98 | package = "aws",
99 | self_package = "github.com/danmx/sigil/pkg/aws",
100 | )
101 |
--------------------------------------------------------------------------------
/pkg/aws/aws.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "os"
7 | "os/exec"
8 | "os/signal"
9 | "syscall"
10 | "time"
11 |
12 | "github.com/danmx/sigil/pkg/aws/helpers"
13 | logger "github.com/danmx/sigil/pkg/aws/log"
14 | sigilOS "github.com/danmx/sigil/pkg/os"
15 |
16 | "github.com/aws/aws-sdk-go/aws"
17 | "github.com/aws/aws-sdk-go/aws/client"
18 | "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
19 | "github.com/aws/aws-sdk-go/aws/session"
20 | "github.com/aws/aws-sdk-go/service/ec2"
21 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
22 | log "github.com/sirupsen/logrus"
23 | )
24 |
25 | // Provider contains necessary components like the session
26 | type Provider struct {
27 | filters Filters
28 | session *session.Session
29 | awsProfile string
30 | }
31 |
32 | // Config contains provider's configuration
33 | type Config struct {
34 | Filters Filters
35 | Region string
36 | Profile string
37 | MFAToken string
38 | Trace bool
39 | }
40 |
41 | // Filters grouped per type
42 | type Filters struct {
43 | Instance InstanceFilters `mapstructure:"instance"`
44 | Session SessionFilters `mapstructure:"session"`
45 | }
46 |
47 | // Instance contain information about the EC2 instance
48 | type Instance struct {
49 | Hostname string `json:"hostname" yaml:"hostname"`
50 | IPAddress string `json:"ip_address" yaml:"ip_address"`
51 | ID string `json:"id" yaml:"id"`
52 | PrivateDNSName string `json:"private_dns_name" yaml:"private_dns_name"`
53 | Name string `json:"name" yaml:"name"`
54 | OSName string `json:"os_name" yaml:"os_name"`
55 | OSType string `json:"os_type" yaml:"os_type"`
56 | OSVersion string `json:"os_version" yaml:"os_version"`
57 | }
58 |
59 | // InstanceFilters contain all types of filters used to limit results
60 | type InstanceFilters struct {
61 | IDs []string `json:"ids" mapstructure:"ids,remain"`
62 | Tags []TagValues `json:"tags" mapstructure:"tags,remain"`
63 | }
64 |
65 | // TagValues contain list of values for specific key
66 | type TagValues struct {
67 | Key string `json:"key" mapstructure:"key,remain"`
68 | Values []string `json:"values" mapstructure:"values,remain"`
69 | }
70 |
71 | // Session contains information about SSM sessions
72 | type Session struct {
73 | SessionID string `json:"session_id" yaml:"session_id"`
74 | Target string `json:"target" yaml:"target"`
75 | Status string `json:"status" yaml:"status"`
76 | StartDate string `json:"start_date" yaml:"start_date"`
77 | Owner string `json:"owner" yaml:"owner"`
78 | }
79 |
80 | // SessionFilters for SSM sessions
81 | type SessionFilters struct {
82 | After string `json:"after" mapstructure:"after,remain"`
83 | Before string `json:"before" mapstructure:"before,remain"`
84 | Target string `json:"target" mapstructure:"target,remain"`
85 | Owner string `json:"owner" mapstructure:"owner,remain"`
86 | }
87 |
88 | const (
89 | execEnvVar = "AWS_EXECUTION_ENV"
90 | maxResults int64 = 50
91 | pluginName = "session-manager-plugin"
92 | // API calls retry configuration
93 | numMaxRetries = 5
94 | minRetryDelay = 10 * time.Millisecond
95 | minThrottleDelay = 500 * time.Millisecond
96 | maxRetryDelay = 1 * time.Second
97 | maxThrottleDelay = 4 * time.Second
98 | // TargetTypeInstanceID points to an instance ID type
99 | TargetTypeInstanceID = "instance-id"
100 | // TargetTypePrivateDNS points to a private DNS type
101 | TargetTypePrivateDNS = "private-dns"
102 | // TargetTypeName points to a name type
103 | TargetTypeName = "name"
104 | )
105 |
106 | // NewWithConfig will generate an AWS Provider with given configuration
107 | func (p *Provider) NewWithConfig(c *Config) error {
108 | options := session.Options{
109 | SharedConfigState: session.SharedConfigEnable,
110 | AssumeRoleTokenProvider: mfaTokenProvider(c.MFAToken),
111 | Profile: c.Profile,
112 | }
113 | awsConfig := aws.NewConfig()
114 | if c.Trace {
115 | awsConfig.LogLevel = aws.LogLevel(aws.LogDebugWithRequestRetries)
116 | awsConfig.Logger = logger.NewTraceLogger()
117 | }
118 | awsConfig.Retryer = client.DefaultRetryer{
119 | NumMaxRetries: numMaxRetries,
120 | MinRetryDelay: minRetryDelay,
121 | MinThrottleDelay: minThrottleDelay,
122 | MaxRetryDelay: maxRetryDelay,
123 | MaxThrottleDelay: maxThrottleDelay,
124 | }
125 | awsConfig.Region = aws.String(c.Region)
126 | options.Config = *awsConfig
127 | sess, err := session.NewSessionWithOptions(options)
128 | if err != nil {
129 | log.WithFields(log.Fields{
130 | "err": err,
131 | "options": options,
132 | }).Error("Failed starting a session")
133 | return err
134 | }
135 | p.session = sess
136 | p.awsProfile = c.Profile
137 | p.filters = c.Filters
138 | return nil
139 | }
140 |
141 | // VerifyDependencies will check all necessary dependencies
142 | func VerifyDependencies() error {
143 | return verifyDependencies(new(helpers.Helpers))
144 | }
145 |
146 | func verifyDependencies(exechelpers helpers.OSExecIface) error {
147 | o, err := exechelpers.LookPath(pluginName)
148 | if err != nil {
149 | err = fmt.Errorf("required plugin not found: %s", err)
150 | log.Error(err)
151 | return err
152 | }
153 | log.WithFields(log.Fields{
154 | "plugin": pluginName,
155 | "path": o,
156 | }).Debugf("%s is installed successfully in %s\n", pluginName, o)
157 | return nil
158 | }
159 |
160 | // AppendUserAgent will add given suffix to HTTP client's user agent
161 | func AppendUserAgent(suffix string) error {
162 | return appendUserAgent(new(helpers.Helpers), suffix)
163 | }
164 |
165 | func appendUserAgent(oshelpers helpers.OSIface, suffix string) error {
166 | value, set := oshelpers.LookupEnv(execEnvVar)
167 | if set {
168 | value += "/"
169 | }
170 | value += suffix
171 | return oshelpers.Setenv(execEnvVar, value)
172 | }
173 |
174 | func (p *Provider) getInstance(targetType, target string) (*ec2.Instance, error) {
175 | return getInstance(ec2.New(p.session), targetType, target)
176 | }
177 |
178 | func getInstance(ec2Client ec2iface.EC2API, targetType, target string) (*ec2.Instance, error) {
179 | if target == "" {
180 | err := errors.New("no target")
181 | log.WithFields(log.Fields{
182 | "target": target,
183 | }).Error(err)
184 | return nil, err
185 | }
186 | filters, err := getFilters(targetType, target)
187 | if err != nil {
188 | return nil, err
189 | }
190 | instance, err := getFirstInstance(ec2Client, filters)
191 | if err != nil {
192 | log.WithFields(log.Fields{
193 | "filters": filters,
194 | }).Error("failed getting the first instance")
195 | return nil, err
196 | }
197 | if instance == nil {
198 | err := fmt.Errorf("no instance that matches the target (%s) and the type (%s)", target, targetType)
199 | log.WithFields(log.Fields{
200 | "targetType": targetType,
201 | "taget": target,
202 | }).Info(err)
203 | return nil, err
204 | }
205 | return instance, nil
206 | }
207 |
208 | func getFilters(targetType, target string) ([]*ec2.Filter, error) {
209 | var filters []*ec2.Filter
210 | switch targetType {
211 | case TargetTypeInstanceID:
212 | filters = []*ec2.Filter{
213 | {
214 | Name: aws.String("instance-id"),
215 | Values: []*string{&target},
216 | },
217 | {
218 | Name: aws.String("instance-state-name"),
219 | Values: []*string{aws.String("running")},
220 | },
221 | }
222 | case TargetTypePrivateDNS:
223 | filters = []*ec2.Filter{
224 | {
225 | Name: aws.String("private-dns-name"),
226 | Values: []*string{&target},
227 | },
228 | {
229 | Name: aws.String("instance-state-name"),
230 | Values: []*string{aws.String("running")},
231 | },
232 | }
233 | case TargetTypeName:
234 | filters = []*ec2.Filter{
235 | {
236 | Name: aws.String("tag:Name"),
237 | Values: []*string{&target},
238 | },
239 | {
240 | Name: aws.String("instance-state-name"),
241 | Values: []*string{aws.String("running")},
242 | },
243 | }
244 | default:
245 | log.WithFields(log.Fields{
246 | "target": target,
247 | "targetType": targetType,
248 | }).Error("unsupported target type")
249 | return nil, fmt.Errorf("unsupported target type: %s", targetType)
250 | }
251 | return filters, nil
252 | }
253 |
254 | func getFirstInstance(ec2Client ec2iface.EC2API, filters []*ec2.Filter) (*ec2.Instance, error) {
255 | input := &ec2.DescribeInstancesInput{
256 | Filters: filters,
257 | MaxResults: aws.Int64(maxResults),
258 | }
259 | var target *ec2.Instance
260 | err := ec2Client.DescribeInstancesPages(input,
261 | func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
262 | for _, reservation := range page.Reservations {
263 | for _, instance := range reservation.Instances {
264 | target = instance
265 | // Escape the function
266 | return false
267 | }
268 | }
269 | return !lastPage
270 | })
271 | if err != nil {
272 | log.WithFields(log.Fields{
273 | "filters": filters,
274 | "DescribeInstancesInput": input,
275 | }).Error("failed DescribeInstancesPages")
276 | return nil, err
277 | }
278 | return target, nil
279 | }
280 |
281 | func mfaTokenProvider(token string) func() (string, error) {
282 | log.WithFields(log.Fields{
283 | "token": token,
284 | }).Debug("Get MFA Token Provider")
285 | if token == "" {
286 | return stscreds.StdinTokenProvider
287 | }
288 | return func() (string, error) {
289 | return token, nil
290 | }
291 | }
292 |
293 | func runSessionPluginManager(payloadJSON, region, profile, inputJSON, endpoint string) error {
294 | log.WithFields(log.Fields{
295 | "payload": payloadJSON,
296 | "region": region,
297 | "profile": profile,
298 | "input": inputJSON,
299 | "endpoint": endpoint,
300 | }).Debug("Inspect session-manager-plugin args")
301 | // TODO allowing logging
302 | // https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html#configure-logs-linux
303 | // https://github.com/aws/aws-cli/blob/5f16b26/awscli/customizations/sessionmanager.py#L83-L89
304 | shell := exec.Command(pluginName, payloadJSON, region, "StartSession", profile, inputJSON, endpoint)
305 | shell.Stdout = os.Stdout
306 | shell.Stdin = os.Stdin
307 | shell.Stderr = os.Stderr
308 | sigilOS.IgnoreUserEnteredSignals()
309 | // This allows to gracefully close the process and execute all defers
310 | signal.Ignore(syscall.SIGHUP)
311 | defer signal.Reset()
312 | err := shell.Run()
313 | if err != nil {
314 | return err
315 | }
316 | return nil
317 | }
318 |
--------------------------------------------------------------------------------
/pkg/aws/aws_test.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | // Helpers Mock generation
4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination helpers_mock_test.go github.com/danmx/sigil/pkg/aws/helpers OSExecIface,OSIface
5 | // AWS EC2 Mock generation
6 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ec2_aws_mock_test.go github.com/aws/aws-sdk-go/service/ec2/ec2iface EC2API
7 |
8 | import (
9 | "errors"
10 | "testing"
11 |
12 | awsCloud "github.com/aws/aws-sdk-go/aws"
13 | _ "github.com/aws/aws-sdk-go/aws/request" // for mocking EC2API
14 | "github.com/aws/aws-sdk-go/service/ec2"
15 | "github.com/golang/mock/gomock"
16 | "github.com/stretchr/testify/assert"
17 | )
18 |
19 | // TestVerifyDependencies verifies the dependency verifier
20 | func TestVerifyDependencies(t *testing.T) {
21 | ctrl := gomock.NewController(t)
22 | defer ctrl.Finish()
23 | m := NewMockOSExecIface(ctrl) // skipcq: SCC-compile
24 |
25 | gomock.InOrder(
26 | m.EXPECT().LookPath(pluginName).Return("", errors.New("")),
27 | )
28 |
29 | assert.Error(t, verifyDependencies(m))
30 |
31 | gomock.InOrder(
32 | m.EXPECT().LookPath(pluginName).Return("/usr/local/bin/session-manager-plugin", nil),
33 | )
34 |
35 | assert.NoError(t, verifyDependencies(m))
36 | }
37 |
38 | // TestAppendUserAgent verifies env var was set
39 | func TestAppendUserAgent(t *testing.T) {
40 | ctrl := gomock.NewController(t)
41 | defer ctrl.Finish()
42 | m := NewMockOSIface(ctrl) // skipcq: SCC-compile
43 |
44 | suffix := "test"
45 |
46 | gomock.InOrder(
47 | m.EXPECT().LookupEnv(execEnvVar).Return("", false),
48 | m.EXPECT().Setenv(execEnvVar, suffix).Return(nil),
49 | )
50 |
51 | assert.NoError(t, appendUserAgent(m, suffix))
52 |
53 | gomock.InOrder(
54 | m.EXPECT().LookupEnv(execEnvVar).Return("value", true),
55 | m.EXPECT().Setenv(execEnvVar, "value/"+suffix).Return(nil),
56 | )
57 |
58 | assert.NoError(t, appendUserAgent(m, suffix))
59 | }
60 |
61 | // TestGetFilters tests fetching instance filters
62 | func TestGetFilters(t *testing.T) {
63 | target := "testTarget"
64 | testMap := map[string][]*ec2.Filter{
65 | TargetTypeInstanceID: {
66 | {
67 | Name: awsCloud.String("instance-id"),
68 | Values: []*string{&target},
69 | },
70 | {
71 | Name: awsCloud.String("instance-state-name"),
72 | Values: []*string{awsCloud.String("running")},
73 | },
74 | },
75 | TargetTypePrivateDNS: {
76 | {
77 | Name: awsCloud.String("private-dns-name"),
78 | Values: []*string{&target},
79 | },
80 | {
81 | Name: awsCloud.String("instance-state-name"),
82 | Values: []*string{awsCloud.String("running")},
83 | },
84 | },
85 | TargetTypeName: {
86 | {
87 | Name: awsCloud.String("tag:Name"),
88 | Values: []*string{&target},
89 | },
90 | {
91 | Name: awsCloud.String("instance-state-name"),
92 | Values: []*string{awsCloud.String("running")},
93 | },
94 | },
95 | }
96 | for key, value := range testMap {
97 | filter, err := getFilters(key, target)
98 | assert.Equal(t, filter, value)
99 | assert.NoError(t, err)
100 | }
101 | _, err := getFilters("invalid", target)
102 | assert.Error(t, err)
103 | }
104 |
105 | // TestGetFirstInstance tests listing the first instance
106 | func TestGetFirstInstance(t *testing.T) {
107 | ctrl := gomock.NewController(t)
108 | defer ctrl.Finish()
109 | m := NewMockEC2API(ctrl) // skipcq: SCC-compile
110 |
111 | filters := []*ec2.Filter{}
112 |
113 | input := &ec2.DescribeInstancesInput{
114 | Filters: filters,
115 | MaxResults: awsCloud.Int64(maxResults),
116 | }
117 |
118 | gomock.InOrder(
119 | m.EXPECT().DescribeInstancesPages(input, gomock.Any()).Return(nil),
120 | m.EXPECT().DescribeInstancesPages(input, gomock.Any()).Return(errors.New("")),
121 | )
122 | _, err := getFirstInstance(m, filters)
123 | assert.NoError(t, err)
124 | _, err = getFirstInstance(m, filters)
125 | assert.Error(t, err)
126 | }
127 |
--------------------------------------------------------------------------------
/pkg/aws/helpers/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library")
2 |
3 | go_library(
4 | name = "go_default_library",
5 | srcs = ["helpers.go"],
6 | importpath = "github.com/danmx/sigil/pkg/aws/helpers",
7 | visibility = ["//visibility:public"],
8 | )
9 |
--------------------------------------------------------------------------------
/pkg/aws/helpers/helpers.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 | )
7 |
8 | type OSExecIface interface {
9 | LookPath(file string) (string, error)
10 | }
11 |
12 | type OSIface interface {
13 | LookupEnv(envVar string) (string, bool)
14 | Setenv(envVar, value string) error
15 | }
16 |
17 | type Helpers struct{}
18 |
19 | // LookupEnv looks up an environment variable
20 | func (Helpers) LookupEnv(envVar string) (string, bool) {
21 | return os.LookupEnv(envVar)
22 | }
23 |
24 | // Setenv sets an environment variable
25 | func (Helpers) Setenv(envVar, value string) error {
26 | return os.Setenv(envVar, value)
27 | }
28 |
29 | // LookPath looks up a path
30 | func (Helpers) LookPath(file string) (string, error) {
31 | return exec.LookPath(file)
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/aws/interface.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | // Cloud wraps init methods used from the aws package
4 | type Cloud interface {
5 | NewWithConfig(c *Config) error
6 | }
7 |
8 | // CloudInstances wraps instances methods used from the aws package
9 | type CloudInstances interface {
10 | Cloud
11 | ListInstances() ([]*Instance, error)
12 | StartSession(targetType, target string) error
13 | }
14 |
15 | // CloudSessions wraps sessions methods used from the aws package
16 | type CloudSessions interface {
17 | Cloud
18 | ListSessions() ([]*Session, error)
19 | TerminateSession(sessionID string) error
20 | }
21 |
22 | // CloudSSH wraps ssh methods used from the aws package
23 | type CloudSSH interface {
24 | Cloud
25 | StartSSH(targetType, target, osUser string, portNumber uint64, publicKey []byte) error
26 | }
27 |
--------------------------------------------------------------------------------
/pkg/aws/list.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "github.com/aws/aws-sdk-go/aws"
5 | "github.com/aws/aws-sdk-go/service/ec2"
6 | "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
7 | "github.com/aws/aws-sdk-go/service/ssm"
8 | "github.com/aws/aws-sdk-go/service/ssm/ssmiface"
9 | log "github.com/sirupsen/logrus"
10 | )
11 |
12 | const capMultiplier = 2
13 |
14 | // ListInstances provides a list of active instances with SSM agent
15 | func (p *Provider) ListInstances() ([]*Instance, error) {
16 | instances, err := fetchInstances(ssm.New(p.session), p.filters.Instance.IDs)
17 | if err != nil {
18 | return nil, err
19 | }
20 | if len(instances) == 0 {
21 | log.Info("No matching instances")
22 | return []*Instance{}, nil
23 | }
24 | filteredInstances, err := filterInstances(ec2.New(p.session), p.filters.Instance.Tags, instances)
25 | if err != nil {
26 | return nil, err
27 | }
28 | return filteredInstances, nil
29 | }
30 |
31 | func fetchInstances(ssmClient ssmiface.SSMAPI, ids []string) (map[string]*Instance, error) {
32 | // Show only instances that have active agents
33 | ssmDescribeInstancesInput := &ssm.DescribeInstanceInformationInput{
34 | MaxResults: aws.Int64(maxResults),
35 | Filters: []*ssm.InstanceInformationStringFilter{
36 | {
37 | Key: aws.String("PingStatus"),
38 | Values: []*string{aws.String("Online")},
39 | },
40 | },
41 | }
42 | if len(ids) > 0 {
43 | log.WithFields(log.Fields{
44 | "ids": ids,
45 | }).Debug("Instance IDs Filter")
46 | ssmDescribeInstancesInput.Filters = append(ssmDescribeInstancesInput.Filters, &ssm.InstanceInformationStringFilter{
47 | Key: aws.String("InstanceIds"),
48 | Values: aws.StringSlice(ids),
49 | })
50 | }
51 |
52 | instances := make(map[string]*Instance)
53 |
54 | err := ssmClient.DescribeInstanceInformationPages(ssmDescribeInstancesInput,
55 | func(page *ssm.DescribeInstanceInformationOutput, lastPage bool) bool {
56 | for _, instance := range page.InstanceInformationList {
57 | log.WithFields(log.Fields{
58 | "InstanceId": *instance.InstanceId,
59 | "ComputerName": *instance.ComputerName,
60 | "IPAddress": *instance.IPAddress,
61 | "PlatformName": *instance.PlatformName,
62 | "PlatformType": *instance.PlatformType,
63 | "PlatformVersion": *instance.PlatformVersion,
64 | }).Debug("Describe Instance")
65 | instances[*instance.InstanceId] = &Instance{
66 | Hostname: *instance.ComputerName,
67 | IPAddress: *instance.IPAddress,
68 | ID: *instance.InstanceId,
69 | OSName: *instance.PlatformName,
70 | OSType: *instance.PlatformType,
71 | OSVersion: *instance.PlatformVersion,
72 | }
73 | }
74 | return !lastPage
75 | })
76 | if err != nil {
77 | log.WithFields(log.Fields{
78 | "input": *ssmDescribeInstancesInput,
79 | "error": err,
80 | }).Error("DescribeInstanceInformationPages")
81 | return nil, err
82 | }
83 | return instances, nil
84 | }
85 |
86 | func filterInstances(ec2Client ec2iface.EC2API, tags []TagValues, instances map[string]*Instance) ([]*Instance, error) {
87 | describeInstancesInput := &ec2.DescribeInstancesInput{
88 | InstanceIds: make([]*string, 0, len(instances)),
89 | Filters: []*ec2.Filter{
90 | {
91 | Name: aws.String("instance-state-name"),
92 | Values: []*string{aws.String("running")},
93 | },
94 | },
95 | }
96 |
97 | for _, tag := range tags {
98 | log.WithFields(log.Fields{
99 | "key": tag.Key,
100 | "values": tag.Values,
101 | }).Debug("Tags Filter")
102 | describeInstancesInput.Filters = append(describeInstancesInput.Filters, &ec2.Filter{
103 | Name: aws.String("tag:" + tag.Key),
104 | Values: aws.StringSlice(tag.Values),
105 | })
106 | }
107 |
108 | outputInstances := make([]*Instance, 0, len(instances))
109 | // Discovering instances private DNS names
110 | for _, instance := range instances {
111 | describeInstancesInput.InstanceIds = append(describeInstancesInput.InstanceIds, &instance.ID)
112 | }
113 | err := ec2Client.DescribeInstancesPages(describeInstancesInput,
114 | func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
115 | for _, reservation := range page.Reservations {
116 | for _, instance := range reservation.Instances {
117 | nameTag := ""
118 | for _, tag := range instance.Tags {
119 | if *tag.Key == "Name" {
120 | nameTag = *tag.Value
121 | break
122 | }
123 | }
124 | instances[*instance.InstanceId].PrivateDNSName = *instance.PrivateDnsName
125 | instances[*instance.InstanceId].Name = nameTag
126 | outputInstances = append(outputInstances, instances[*instance.InstanceId])
127 | }
128 | }
129 | return !lastPage
130 | })
131 | if err != nil {
132 | log.WithFields(log.Fields{
133 | "input": *describeInstancesInput,
134 | "error": err,
135 | }).Error("DescribeInstancesPages")
136 | return nil, err
137 | }
138 | return outputInstances, nil
139 | }
140 |
141 | // ListSessions provides a list of active SSM sessions
142 | func (p *Provider) ListSessions() ([]*Session, error) {
143 | return listSessions(ssm.New(p.session), p.filters.Session)
144 | }
145 |
146 | func listSessions(ssmClient ssmiface.SSMAPI, sessionFilters SessionFilters) ([]*Session, error) {
147 | // Show only connected sessions
148 | ssmDescribeSessionsInput := &ssm.DescribeSessionsInput{
149 | State: aws.String("Active"),
150 | }
151 | // Parse filters
152 | filters := []*ssm.SessionFilter{}
153 | if sessionFilters.After != "" {
154 | filters = append(filters, &ssm.SessionFilter{
155 | Key: aws.String("InvokedAfter"),
156 | Value: &sessionFilters.After,
157 | })
158 | }
159 | if sessionFilters.Before != "" {
160 | filters = append(filters, &ssm.SessionFilter{
161 | Key: aws.String("InvokedBefore"),
162 | Value: &sessionFilters.Before,
163 | })
164 | }
165 | if sessionFilters.Target != "" {
166 | filters = append(filters, &ssm.SessionFilter{
167 | Key: aws.String("Target"),
168 | Value: &sessionFilters.Target,
169 | })
170 | }
171 | if sessionFilters.Owner != "" {
172 | filters = append(filters, &ssm.SessionFilter{
173 | Key: aws.String("Owner"),
174 | Value: &sessionFilters.Owner,
175 | })
176 | }
177 | if len(filters) > 0 {
178 | ssmDescribeSessionsInput.Filters = filters
179 | }
180 | sessions := []*Session{}
181 | for out, err := ssmClient.DescribeSessions(ssmDescribeSessionsInput); ; {
182 | if err != nil {
183 | log.WithField("error", err).Error("DescribeSessions")
184 | return nil, err
185 | }
186 | log.WithField("sessions array len", len(out.Sessions)).Debug("Sessions Output")
187 | if len(sessions)+1 > cap(sessions) {
188 | newSlice := make([]*Session, len(sessions), (cap(sessions))*capMultiplier)
189 | n := copy(newSlice, sessions)
190 | log.WithField("no. copied elements", n).Debug("Expand Sessions slice")
191 | sessions = newSlice
192 | }
193 | for i, sess := range out.Sessions {
194 | log.WithField("session", sess).Debugf("Single session #%d", i)
195 | startDate, err := sess.StartDate.MarshalText()
196 | if err != nil {
197 | log.WithField("error", err).Error("StartDate MarshalText")
198 | return nil, err
199 | }
200 | startDateString := string(startDate)
201 | sessions = append(sessions, &Session{
202 | SessionID: *sess.SessionId,
203 | Target: *sess.Target,
204 | Status: *sess.Status,
205 | StartDate: startDateString,
206 | Owner: *sess.Owner,
207 | })
208 | }
209 | if out.NextToken == nil {
210 | break
211 | }
212 | ssmDescribeSessionsInput.NextToken = out.NextToken
213 | }
214 | return sessions, nil
215 | }
216 |
--------------------------------------------------------------------------------
/pkg/aws/list_test.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | // AWS SSM Mock generation
4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ssm_aws_mock_test.go github.com/aws/aws-sdk-go/service/ssm/ssmiface SSMAPI
5 | // AWS EC2 Mock generation
6 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ec2_aws_mock_test.go github.com/aws/aws-sdk-go/service/ec2/ec2iface EC2API
7 |
8 | import (
9 | "testing"
10 |
11 | "github.com/aws/aws-sdk-go/aws"
12 | "github.com/aws/aws-sdk-go/service/ec2"
13 | "github.com/aws/aws-sdk-go/service/ssm"
14 | "github.com/golang/mock/gomock"
15 | "github.com/stretchr/testify/assert"
16 | )
17 |
18 | // TestFetchInstances verifies instances fetching
19 | func TestFetchInstances(t *testing.T) {
20 | ctrl := gomock.NewController(t)
21 | defer ctrl.Finish()
22 | m := NewMockSSMAPI(ctrl) // skipcq: SCC-compile
23 |
24 | IDs := []string{"1", "2", "3"}
25 |
26 | instances := make(map[string]*Instance)
27 |
28 | input := &ssm.DescribeInstanceInformationInput{
29 | MaxResults: aws.Int64(maxResults),
30 | Filters: []*ssm.InstanceInformationStringFilter{
31 | {
32 | Key: aws.String("PingStatus"),
33 | Values: []*string{aws.String("Online")},
34 | },
35 | {
36 | Key: aws.String("InstanceIds"),
37 | Values: aws.StringSlice(IDs),
38 | },
39 | },
40 | }
41 |
42 | gomock.InOrder(
43 | m.EXPECT().DescribeInstanceInformationPages(input, gomock.Any()).Return(nil),
44 | )
45 | list, err := fetchInstances(m, IDs)
46 | assert.Equal(t, instances, list)
47 | assert.NoError(t, err)
48 | }
49 |
50 | // TestFilterInstances verifies instances filtering
51 | func TestFilterInstances(t *testing.T) {
52 | ctrl := gomock.NewController(t)
53 | defer ctrl.Finish()
54 | m := NewMockEC2API(ctrl) // skipcq: SCC-compile
55 |
56 | tags := []TagValues{
57 | {
58 | Key: "TestKey1",
59 | Values: []string{"1"},
60 | },
61 | }
62 |
63 | instances := make(map[string]*Instance)
64 |
65 | outputInstances := []*Instance{}
66 |
67 | input := &ec2.DescribeInstancesInput{
68 | InstanceIds: make([]*string, 0, len(instances)),
69 | Filters: []*ec2.Filter{
70 | {
71 | Name: aws.String("instance-state-name"),
72 | Values: []*string{aws.String("running")},
73 | },
74 | },
75 | }
76 | for _, tag := range tags {
77 | input.Filters = append(input.Filters, &ec2.Filter{
78 | Name: aws.String("tag:" + tag.Key),
79 | Values: aws.StringSlice(tag.Values),
80 | })
81 | }
82 | for _, instance := range instances {
83 | input.InstanceIds = append(input.InstanceIds, &instance.ID)
84 | }
85 |
86 | gomock.InOrder(
87 | m.EXPECT().DescribeInstancesPages(input, gomock.Any()).Return(nil),
88 | )
89 | list, err := filterInstances(m, tags, instances)
90 | assert.Equal(t, outputInstances, list)
91 | assert.NoError(t, err)
92 | }
93 |
94 | // TestListSessions verifies session listing
95 | func TestListSessions(t *testing.T) {
96 | ctrl := gomock.NewController(t)
97 | defer ctrl.Finish()
98 | m := NewMockSSMAPI(ctrl) // skipcq: SCC-compile
99 |
100 | filters := SessionFilters{
101 | After: "after",
102 | Before: "before",
103 | Target: "target",
104 | Owner: "owner",
105 | }
106 |
107 | sessions := []*Session{}
108 |
109 | input := &ssm.DescribeSessionsInput{
110 | State: aws.String("Active"),
111 | Filters: []*ssm.SessionFilter{
112 | {
113 | Key: aws.String("InvokedAfter"),
114 | Value: &filters.After,
115 | },
116 | {
117 | Key: aws.String("InvokedBefore"),
118 | Value: &filters.Before,
119 | },
120 | {
121 | Key: aws.String("Target"),
122 | Value: &filters.Target,
123 | },
124 | {
125 | Key: aws.String("Owner"),
126 | Value: &filters.Owner,
127 | },
128 | },
129 | }
130 |
131 | output := &ssm.DescribeSessionsOutput{
132 | NextToken: nil,
133 | Sessions: []*ssm.Session{},
134 | }
135 |
136 | gomock.InOrder(
137 | m.EXPECT().DescribeSessions(input).Return(output, nil),
138 | )
139 |
140 | list, err := listSessions(m, filters)
141 | assert.Equal(t, sessions, list)
142 | assert.NoError(t, err)
143 | }
144 |
--------------------------------------------------------------------------------
/pkg/aws/log/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library")
2 |
3 | go_library(
4 | name = "go_default_library",
5 | srcs = ["log.go"],
6 | importpath = "github.com/danmx/sigil/pkg/aws/log",
7 | visibility = ["//visibility:public"],
8 | deps = [
9 | "@com_github_aws_aws_sdk_go//aws:go_default_library",
10 | "@com_github_sirupsen_logrus//:go_default_library",
11 | ],
12 | )
13 |
--------------------------------------------------------------------------------
/pkg/aws/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "github.com/aws/aws-sdk-go/aws"
5 | logr "github.com/sirupsen/logrus"
6 | )
7 |
8 | // A traceLogger provides a minimalistic logger satisfying the aws.Logger interface.
9 | type traceLogger struct{}
10 |
11 | // newTraceLogger returns a Logger which will write log messages to current logger
12 | func NewTraceLogger() aws.Logger {
13 | return &traceLogger{}
14 | }
15 |
16 | // Log logs the parameters to the stdlib logger. See log.Println.
17 | func (traceLogger) Log(args ...interface{}) {
18 | logr.Trace(args...)
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/aws/session.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 |
7 | "github.com/aws/aws-sdk-go/service/ssm"
8 | "github.com/aws/aws-sdk-go/service/ssm/ssmiface"
9 | log "github.com/sirupsen/logrus"
10 | )
11 |
12 | // StartSession will start a session for a chosed node
13 | func (p *Provider) StartSession(targetType, target string) error {
14 | instance, err := p.getInstance(targetType, target)
15 | if err != nil {
16 | log.WithFields(log.Fields{
17 | "targetType": targetType,
18 | "target": target,
19 | }).Error("failed getting the instance")
20 | return err
21 | }
22 | log.WithField("target instance id", *instance.InstanceId).Debug("Checking the target instance ID")
23 | startSessionInput := &ssm.StartSessionInput{
24 | Target: instance.InstanceId,
25 | }
26 | ssmClient := ssm.New(p.session)
27 | output, err := ssmClient.StartSession(startSessionInput)
28 | if err != nil {
29 | return err
30 | }
31 |
32 | defer func() {
33 | if err = terminateSession(ssmClient, *output.SessionId); err != nil {
34 | err = fmt.Errorf("failed terminating the session (it could be already terminated): %e", err)
35 | log.Warn(err)
36 | }
37 | }()
38 |
39 | log.WithFields(log.Fields{
40 | "sessionID": *output.SessionId,
41 | "streamURL": *output.StreamUrl,
42 | "token": *output.TokenValue,
43 | }).Debug("SSM Start Session Output")
44 | payload, err := json.Marshal(output)
45 | if err != nil {
46 | return err
47 | }
48 |
49 | startSessionInputJSON, err := json.Marshal(startSessionInput)
50 | if err != nil {
51 | return err
52 | }
53 |
54 | return runSessionPluginManager(string(payload), *p.session.Config.Region, p.awsProfile, string(startSessionInputJSON), ssmClient.Client.Endpoint)
55 | }
56 |
57 | // TerminateSession will close chosed active session
58 | func (p *Provider) TerminateSession(sessionID string) error {
59 | return terminateSession(ssm.New(p.session), sessionID)
60 | }
61 |
62 | func terminateSession(ssmClient ssmiface.SSMAPI, sessionID string) error {
63 | _, err := ssmClient.TerminateSession(&ssm.TerminateSessionInput{
64 | SessionId: &sessionID,
65 | })
66 | if err != nil {
67 | log.WithFields(log.Fields{"sessionID": sessionID}).Warn(err)
68 | return err
69 | }
70 | return nil
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/aws/session_test.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | // AWS SSM Mock generation
4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ssm_aws_mock_test.go github.com/aws/aws-sdk-go/service/ssm/ssmiface SSMAPI
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/aws/aws-sdk-go/service/ssm"
10 | "github.com/golang/mock/gomock"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | // TestTerminateSession verifies session termination
15 | func TestTerminateSession(t *testing.T) {
16 | ctrl := gomock.NewController(t)
17 | defer ctrl.Finish()
18 | m := NewMockSSMAPI(ctrl) // skipcq: SCC-compile
19 |
20 | sessionID := "testID"
21 |
22 | gomock.InOrder(
23 | m.EXPECT().TerminateSession(&ssm.TerminateSessionInput{
24 | SessionId: &sessionID,
25 | }).Return(new(ssm.TerminateSessionOutput), nil),
26 | )
27 |
28 | assert.NoError(t, terminateSession(m, sessionID))
29 | }
30 |
--------------------------------------------------------------------------------
/pkg/aws/ssh.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "strconv"
7 |
8 | "github.com/aws/aws-sdk-go/aws"
9 | "github.com/aws/aws-sdk-go/service/ec2instanceconnect"
10 | "github.com/aws/aws-sdk-go/service/ec2instanceconnect/ec2instanceconnectiface"
11 | "github.com/aws/aws-sdk-go/service/ssm"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | // StartSSH will start a ssh proxy session for a chosed node
16 | func (p *Provider) StartSSH(targetType, target, osUser string, portNumber uint64, publicKey []byte) error {
17 | instance, err := p.getInstance(targetType, target)
18 | if err != nil {
19 | return err
20 | }
21 |
22 | if len(publicKey) > 0 {
23 | svc := ec2instanceconnect.New(p.session)
24 | err = uploadPublicKey(svc, publicKey, osUser, *instance.InstanceId, *instance.Placement.AvailabilityZone)
25 | if err != nil {
26 | return err
27 | }
28 | }
29 |
30 | ssmClient := ssm.New(p.session)
31 | parameters := map[string][]*string{
32 | "portNumber": {aws.String(strconv.FormatUint(portNumber, 10))}, //nolint:gomnd // decimal base
33 | }
34 | startSessionInput := &ssm.StartSessionInput{
35 | Parameters: parameters,
36 | Target: instance.InstanceId,
37 | DocumentName: aws.String("AWS-StartSSHSession"),
38 | }
39 | output, err := ssmClient.StartSession(startSessionInput)
40 | if err != nil {
41 | return err
42 | }
43 |
44 | defer func() {
45 | if err = terminateSession(ssmClient, *output.SessionId); err != nil {
46 | err = fmt.Errorf("failed terminating the session (it could be already terminated): %e", err)
47 | log.Warn(err)
48 | }
49 | }()
50 |
51 | log.WithFields(log.Fields{
52 | "sessionID": *output.SessionId,
53 | "streamURL": *output.StreamUrl,
54 | "token": *output.TokenValue,
55 | }).Debug("SSM Start Session Output")
56 | payload, err := json.Marshal(output)
57 | if err != nil {
58 | return err
59 | }
60 |
61 | startSessionInputJSON, err := json.Marshal(startSessionInput)
62 | if err != nil {
63 | return err
64 | }
65 |
66 | endpoint := ssmClient.Client.Endpoint
67 |
68 | return runSessionPluginManager(string(payload), *p.session.Config.Region, p.awsProfile, string(startSessionInputJSON), endpoint)
69 | }
70 |
71 | func uploadPublicKey(client ec2instanceconnectiface.EC2InstanceConnectAPI, publicKey []byte, osUser, instanceID, availabilityZone string) error {
72 | pubKey := string(publicKey)
73 | log.WithFields(log.Fields{
74 | "SSHPublicKey": pubKey,
75 | "InstanceOSUser": osUser,
76 | "InstanceId": instanceID,
77 | "AvailabilityZone": availabilityZone,
78 | }).Debug("SendSSHPublicKey")
79 |
80 | out, err := client.SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{
81 | AvailabilityZone: &availabilityZone,
82 | InstanceId: &instanceID,
83 | InstanceOSUser: &osUser,
84 | SSHPublicKey: &pubKey,
85 | })
86 | if err != nil {
87 | log.WithFields(log.Fields{
88 | "AvailabilityZone": availabilityZone,
89 | "InstanceID": instanceID,
90 | "InstanceOSUser": osUser,
91 | "SSHPublicKey": pubKey,
92 | "error": err,
93 | }).Error("failed SendSSHPublicKey")
94 | return err
95 | }
96 | if !*out.Success {
97 | return fmt.Errorf("failed SendSSHPublicKey. RequestID: %s", *out.RequestId)
98 | }
99 | return nil
100 | }
101 |
--------------------------------------------------------------------------------
/pkg/aws/ssh_test.go:
--------------------------------------------------------------------------------
1 | package aws
2 |
3 | // AWS EC2 Instance Connect Mock generation
4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/aws -package aws -destination ec2instanceconnect_aws_mock_test.go github.com/aws/aws-sdk-go/service/ec2instanceconnect/ec2instanceconnectiface EC2InstanceConnectAPI
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/aws/aws-sdk-go/aws"
10 | "github.com/aws/aws-sdk-go/service/ec2instanceconnect"
11 | "github.com/golang/mock/gomock"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | // TestUploadPublicKey verifies upload of a public key
16 | func TestUploadPublicKey(t *testing.T) {
17 | ctrl := gomock.NewController(t)
18 | defer ctrl.Finish()
19 | m := NewMockEC2InstanceConnectAPI(ctrl) // skipcq: SCC-compile
20 |
21 | availabilityZone := "a"
22 | instanceID := "1"
23 | osUser := "ec2user"
24 | pubKey := "testKey"
25 |
26 | output := &ec2instanceconnect.SendSSHPublicKeyOutput{
27 | RequestId: aws.String("testSuccess"),
28 | Success: aws.Bool(true),
29 | }
30 |
31 | gomock.InOrder(
32 | m.EXPECT().SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{
33 | AvailabilityZone: &availabilityZone,
34 | InstanceId: &instanceID,
35 | InstanceOSUser: &osUser,
36 | SSHPublicKey: &pubKey,
37 | }).Return(output, nil),
38 | )
39 |
40 | assert.NoError(t, uploadPublicKey(m, []byte(pubKey), osUser, instanceID, availabilityZone))
41 |
42 | output = &ec2instanceconnect.SendSSHPublicKeyOutput{
43 | Success: aws.Bool(false),
44 | RequestId: aws.String("testFailure"),
45 | }
46 |
47 | gomock.InOrder(
48 | m.EXPECT().SendSSHPublicKey(&ec2instanceconnect.SendSSHPublicKeyInput{
49 | AvailabilityZone: &availabilityZone,
50 | InstanceId: &instanceID,
51 | InstanceOSUser: &osUser,
52 | SSHPublicKey: &pubKey,
53 | }).Return(output, nil),
54 | )
55 |
56 | assert.Error(t, uploadPublicKey(m, []byte(pubKey), osUser, instanceID, availabilityZone))
57 | }
58 |
--------------------------------------------------------------------------------
/pkg/list/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock")
3 |
4 | go_library(
5 | name = "go_default_library",
6 | srcs = ["list.go"],
7 | importpath = "github.com/danmx/sigil/pkg/list",
8 | visibility = ["//visibility:public"],
9 | deps = [
10 | "//pkg/aws:go_default_library",
11 | "@com_github_sirupsen_logrus//:go_default_library",
12 | "@in_gopkg_yaml_v2//:go_default_library",
13 | ],
14 | )
15 |
16 | go_test(
17 | name = "go_default_test",
18 | srcs = [
19 | "aws_mock_test.go",
20 | "list_test.go",
21 | ],
22 | embed = [":go_default_library"],
23 | deps = [
24 | "//pkg/aws:go_default_library",
25 | "@com_github_golang_mock//gomock:go_default_library",
26 | "@com_github_stretchr_testify//assert:go_default_library",
27 | ],
28 | )
29 |
30 | gomock(
31 | name = "mock_aws",
32 | out = "aws_mock_test.go",
33 | interfaces = [
34 | "Cloud",
35 | "CloudInstances",
36 | "CloudSessions",
37 | "CloudSSH",
38 | ],
39 | library = "//pkg/aws:go_default_library",
40 | package = "list",
41 | self_package = "github.com/danmx/sigil/pkg/list",
42 | )
43 |
--------------------------------------------------------------------------------
/pkg/list/list.go:
--------------------------------------------------------------------------------
1 | package list
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "encoding/json"
7 | "fmt"
8 | "os"
9 | "strconv"
10 | "strings"
11 | "text/tabwriter"
12 |
13 | "github.com/danmx/sigil/pkg/aws"
14 |
15 | log "github.com/sirupsen/logrus"
16 | yaml "gopkg.in/yaml.v2"
17 | )
18 |
19 | // List wraps methods used from the pkg/list package
20 | type List interface {
21 | Start(input *StartInput) error
22 | }
23 |
24 | const (
25 | // FormatText points to the text format type
26 | FormatText = "text"
27 | // FormatWide points to the wide format type
28 | FormatWide = "wide"
29 | // FormatJSON points to the json format type
30 | FormatJSON = "json"
31 | // FormatYAML points to the yaml format type
32 | FormatYAML = "yaml"
33 | // TypeListInstances points to the instances list type
34 | TypeListInstances = "instances"
35 | // TypeListSessions points to the sessions list type
36 | TypeListSessions = "sessions"
37 | tabPadding = 2
38 | )
39 |
40 | // StartInput struct contains all input data
41 | type StartInput struct {
42 | // Define output format
43 | OutputFormat *string
44 | Interactive *bool
45 | Type *string
46 | MFAToken *string
47 | Region *string
48 | Profile *string
49 | Filters *aws.Filters
50 | Trace *bool
51 | }
52 |
53 | // Start will output a ist of all available EC2 instances
54 | func Start(input *StartInput) error {
55 | provider := aws.Provider{}
56 | err := provider.NewWithConfig(&aws.Config{
57 | Filters: *input.Filters,
58 | Region: *input.Region,
59 | Profile: *input.Profile,
60 | MFAToken: *input.MFAToken,
61 | Trace: *input.Trace,
62 | })
63 | if err != nil {
64 | log.Error(err)
65 | return err
66 | }
67 | switch *input.Type {
68 | case TypeListInstances:
69 | err := input.listInstances(&provider)
70 | if err != nil {
71 | return err
72 | }
73 | case TypeListSessions:
74 | err := input.listSessions(&provider)
75 | if err != nil {
76 | return err
77 | }
78 | default:
79 | err := fmt.Errorf("unsupported list type: %s", *input.Type)
80 | log.WithField("type", *input.Type).Error(err)
81 | return err
82 | }
83 | return nil
84 | }
85 |
86 | func sessionsToString(format string, sessions []*aws.Session) (string, error) {
87 | switch format {
88 | case FormatText:
89 | buf := bytes.NewBufferString("")
90 | w := new(tabwriter.Writer)
91 | w.Init(buf, 0, 0, tabPadding, ' ', 0)
92 | fmt.Fprintln(w, "Index\tSession ID\tTarget\tStart Date")
93 | for i, session := range sessions {
94 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\n",
95 | (i + 1), session.SessionID, session.Target, session.StartDate)
96 | }
97 | err := w.Flush()
98 | if err != nil {
99 | return "", err
100 | }
101 | return buf.String(), nil
102 | case FormatJSON:
103 | data, err := json.Marshal(sessions)
104 | if err != nil {
105 | return "", err
106 | }
107 | // JSON output was missing new line
108 | return string(data) + "\n", nil
109 | case FormatYAML:
110 | data, err := yaml.Marshal(sessions)
111 | if err != nil {
112 | return "", err
113 | }
114 | return string(data), nil
115 | case FormatWide:
116 | buf := new(bytes.Buffer)
117 | w := new(tabwriter.Writer)
118 | w.Init(buf, 0, 0, tabPadding, ' ', 0)
119 | fmt.Fprintln(w, "Index\tSession ID\tTarget\tStart Date\tOwner\tStatus")
120 | for i, session := range sessions {
121 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\n",
122 | (i + 1), session.SessionID, session.Target, session.StartDate,
123 | session.Owner, session.Status)
124 | }
125 | err := w.Flush()
126 | if err != nil {
127 | return "", err
128 | }
129 | return buf.String(), nil
130 | default:
131 | return "", fmt.Errorf("unsupported output format: %s", format)
132 | }
133 | }
134 |
135 | func instancesToString(format string, instances []*aws.Instance) (string, error) {
136 | switch format {
137 | case FormatText:
138 | buf := bytes.NewBufferString("")
139 | w := new(tabwriter.Writer)
140 | w.Init(buf, 0, 0, tabPadding, ' ', 0)
141 | fmt.Fprintln(w, "Index\tName\tInstance ID\tIP Address\tPrivate DNS Name")
142 | for i, instance := range instances {
143 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\n",
144 | (i + 1), instance.Name, instance.ID, instance.IPAddress, instance.PrivateDNSName)
145 | }
146 | err := w.Flush()
147 | if err != nil {
148 | return "", err
149 | }
150 | return buf.String(), nil
151 | case FormatJSON:
152 | data, err := json.Marshal(instances)
153 | if err != nil {
154 | return "", err
155 | }
156 | // JSON output was missing new line
157 | return string(data) + "\n", nil
158 | case FormatYAML:
159 | data, err := yaml.Marshal(instances)
160 | if err != nil {
161 | return "", err
162 | }
163 | return string(data), nil
164 | case FormatWide:
165 | buf := new(bytes.Buffer)
166 | w := new(tabwriter.Writer)
167 | w.Init(buf, 0, 0, tabPadding, ' ', 0)
168 | fmt.Fprintln(w, "Index\tName\tInstance ID\tIP Address\tPrivate DNS Name\tHostname\tOS Name\tOS Version\tOS Type")
169 | for i, instance := range instances {
170 | fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
171 | (i + 1), instance.Name, instance.ID, instance.IPAddress, instance.PrivateDNSName,
172 | instance.Hostname, instance.OSName, instance.OSVersion, instance.OSType)
173 | }
174 | err := w.Flush()
175 | if err != nil {
176 | return "", err
177 | }
178 | return buf.String(), nil
179 | default:
180 | return "", fmt.Errorf("unsupported output format: %s", format)
181 | }
182 | }
183 |
184 | func (input *StartInput) listInstances(provider aws.CloudInstances) error {
185 | instances, err := provider.ListInstances()
186 | if err != nil {
187 | log.Error("Failed listing instances")
188 | return err
189 | }
190 | outString, err := instancesToString(*input.OutputFormat, instances)
191 | if err != nil {
192 | log.Error("Failed stringifying instances")
193 | return err
194 | }
195 | // TODO Mock stdout
196 | fmt.Fprint(os.Stdout, outString)
197 | if *input.Interactive && len(instances) > 0 {
198 | // TODO Mock stdin and stderr
199 | reader := bufio.NewReader(os.Stdin)
200 | fmt.Fprintf(os.Stderr, "Choose an instance to connect to [1 - %d]: ", len(instances))
201 | textInput, err := reader.ReadString('\n')
202 | if err != nil {
203 | return err
204 | }
205 | i, err := strconv.Atoi(strings.ReplaceAll(textInput, "\n", ""))
206 | if err != nil {
207 | return err
208 | }
209 | log.WithField("index", i).Debug("Picked EC2 Instance")
210 | if i < 1 || i > len(instances) {
211 | return fmt.Errorf("instance index out of range: %d", i)
212 | }
213 | instance := instances[i-1]
214 | err = provider.StartSession(aws.TargetTypeInstanceID, instance.ID)
215 | if err != nil {
216 | return err
217 | }
218 | }
219 | return nil
220 | }
221 |
222 | func (input *StartInput) listSessions(provider aws.CloudSessions) error {
223 | sessions, err := provider.ListSessions()
224 | if err != nil {
225 | log.Error("Failed listing instances")
226 | return err
227 | }
228 | outString, err := sessionsToString(*input.OutputFormat, sessions)
229 | if err != nil {
230 | log.Error("Failed stringifying instances")
231 | return err
232 | }
233 | // TODO Mock stdout
234 | fmt.Fprint(os.Stdout, outString)
235 | if *input.Interactive && len(sessions) > 0 {
236 | // TODO Mock stdin and stderr
237 | reader := bufio.NewReader(os.Stdin)
238 | fmt.Fprintf(os.Stderr, "Terminate session [1 - %d]: ", len(sessions))
239 | textInput, err := reader.ReadString('\n')
240 | if err != nil {
241 | return err
242 | }
243 | i, err := strconv.Atoi(strings.ReplaceAll(textInput, "\n", ""))
244 | if err != nil {
245 | return err
246 | }
247 | log.WithField("index", i).Debug("Picked session")
248 | if i < 1 || i > len(sessions) {
249 | return fmt.Errorf("session index out of range: %d", i)
250 | }
251 | chosenSession := sessions[i-1]
252 | err = provider.TerminateSession(chosenSession.SessionID)
253 | if err != nil {
254 | return err
255 | }
256 | }
257 | return nil
258 | }
259 |
--------------------------------------------------------------------------------
/pkg/list/list_test.go:
--------------------------------------------------------------------------------
1 | package list
2 |
3 | // AWS Mock generation
4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/list -package list -destination aws_mock_test.go github.com/danmx/sigil/pkg/aws Cloud,CloudInstances,CloudSessions,CloudSSH
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/danmx/sigil/pkg/aws"
10 |
11 | "github.com/golang/mock/gomock"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | // TestInstancesString verifies correctness of preparing a list of instanes
16 | func TestInstancesString(t *testing.T) {
17 | resultText := `Index Name Instance ID IP Address Private DNS Name
18 | 1 testNode1 i-xxxxxxxxxxxxxxxx1 10.10.10.1 test1.local
19 | 2 testNode2 i-xxxxxxxxxxxxxxxx2 10.10.10.2 test2.local
20 | `
21 | resultWide := `Index Name Instance ID IP Address Private DNS Name Hostname OS Name OS Version OS Type
22 | 1 testNode1 i-xxxxxxxxxxxxxxxx1 10.10.10.1 test1.local testHostname1 Amazon Linux 2 Linux
23 | 2 testNode2 i-xxxxxxxxxxxxxxxx2 10.10.10.2 test2.local testHostname2 Ubuntu 18.04 Linux
24 | `
25 | resultJSON := `[{"hostname":"testHostname1","ip_address":"10.10.10.1","id":"i-xxxxxxxxxxxxxxxx1","private_dns_name":"test1.local","name":"testNode1","os_name":"Amazon Linux","os_type":"Linux","os_version":"2"},{"hostname":"testHostname2","ip_address":"10.10.10.2","id":"i-xxxxxxxxxxxxxxxx2","private_dns_name":"test2.local","name":"testNode2","os_name":"Ubuntu","os_type":"Linux","os_version":"18.04"}]
26 | `
27 | resultYAML := `- hostname: testHostname1
28 | ip_address: 10.10.10.1
29 | id: i-xxxxxxxxxxxxxxxx1
30 | private_dns_name: test1.local
31 | name: testNode1
32 | os_name: Amazon Linux
33 | os_type: Linux
34 | os_version: "2"
35 | - hostname: testHostname2
36 | ip_address: 10.10.10.2
37 | id: i-xxxxxxxxxxxxxxxx2
38 | private_dns_name: test2.local
39 | name: testNode2
40 | os_name: Ubuntu
41 | os_type: Linux
42 | os_version: "18.04"
43 | `
44 |
45 | instances := []*aws.Instance{
46 | {
47 | Hostname: "testHostname1",
48 | IPAddress: "10.10.10.1",
49 | ID: "i-xxxxxxxxxxxxxxxx1",
50 | PrivateDNSName: "test1.local",
51 | Name: "testNode1",
52 | OSName: "Amazon Linux",
53 | OSType: "Linux",
54 | OSVersion: "2",
55 | },
56 | {
57 | Hostname: "testHostname2",
58 | IPAddress: "10.10.10.2",
59 | ID: "i-xxxxxxxxxxxxxxxx2",
60 | PrivateDNSName: "test2.local",
61 | Name: "testNode2",
62 | OSName: "Ubuntu",
63 | OSType: "Linux",
64 | OSVersion: "18.04",
65 | },
66 | }
67 |
68 | a := assert.New(t)
69 |
70 | _, err := instancesToString("wrong", instances)
71 | a.NotNil(err)
72 | outString, err := instancesToString(FormatText, instances)
73 | a.Nil(err)
74 | a.Equal(resultText, outString)
75 | outString, err = instancesToString(FormatWide, instances)
76 | a.Nil(err)
77 | a.Equal(resultWide, outString)
78 | outString, err = instancesToString(FormatJSON, instances)
79 | a.Nil(err)
80 | a.Equal(resultJSON, outString)
81 | outString, err = instancesToString(FormatYAML, instances)
82 | a.Nil(err)
83 | a.Equal(resultYAML, outString)
84 | }
85 |
86 | // TestSessionsString verifies correctness of preparing a list of sessions
87 | func TestSessionsString(t *testing.T) {
88 | resultText := `Index Session ID Target Start Date
89 | 1 test-1234567890 i-xxxxxxxxxxxxxxxx1 2019-05-03T10:08:44Z
90 | `
91 | resultWide := `Index Session ID Target Start Date Owner Status
92 | 1 test-1234567890 i-xxxxxxxxxxxxxxxx1 2019-05-03T10:08:44Z arn:aws:sts::0123456789:assumed-role/test/test Connected
93 | `
94 | resultJSON := `[{"session_id":"test-1234567890","target":"i-xxxxxxxxxxxxxxxx1","status":"Connected","start_date":"2019-05-03T10:08:44Z","owner":"arn:aws:sts::0123456789:assumed-role/test/test"}]
95 | `
96 | resultYAML := `- session_id: test-1234567890
97 | target: i-xxxxxxxxxxxxxxxx1
98 | status: Connected
99 | start_date: "2019-05-03T10:08:44Z"
100 | owner: arn:aws:sts::0123456789:assumed-role/test/test
101 | `
102 |
103 | sessions := []*aws.Session{
104 | {
105 | SessionID: "test-1234567890",
106 | Target: "i-xxxxxxxxxxxxxxxx1",
107 | Status: "Connected",
108 | StartDate: "2019-05-03T10:08:44Z",
109 | Owner: "arn:aws:sts::0123456789:assumed-role/test/test",
110 | },
111 | }
112 |
113 | a := assert.New(t)
114 |
115 | _, err := sessionsToString("wrong", sessions)
116 | a.NotNil(err)
117 | outString, err := sessionsToString(FormatText, sessions)
118 | a.Nil(err)
119 | a.Equal(resultText, outString)
120 | outString, err = sessionsToString(FormatWide, sessions)
121 | a.Nil(err)
122 | a.Equal(resultWide, outString)
123 | outString, err = sessionsToString(FormatJSON, sessions)
124 | a.Nil(err)
125 | a.Equal(resultJSON, outString)
126 | outString, err = sessionsToString(FormatYAML, sessions)
127 | a.Nil(err)
128 | a.Equal(resultYAML, outString)
129 | }
130 |
131 | // TestListInstances verifies listing instances
132 | func TestListInstances(t *testing.T) {
133 | instances := []*aws.Instance{
134 | {
135 | Hostname: "testHostname1",
136 | IPAddress: "10.10.10.1",
137 | ID: "i-xxxxxxxxxxxxxxxx1",
138 | PrivateDNSName: "test1.local",
139 | Name: "testNode1",
140 | OSName: "Amazon Linux",
141 | OSType: "Linux",
142 | OSVersion: "2",
143 | },
144 | {
145 | Hostname: "testHostname2",
146 | IPAddress: "10.10.10.2",
147 | ID: "i-xxxxxxxxxxxxxxxx2",
148 | PrivateDNSName: "test2.local",
149 | Name: "testNode2",
150 | OSName: "Ubuntu",
151 | OSType: "Linux",
152 | OSVersion: "18.04",
153 | },
154 | }
155 | interactive := false
156 | format := FormatText
157 | input := StartInput{
158 | OutputFormat: &format,
159 | Interactive: &interactive,
160 | }
161 |
162 | ctrl := gomock.NewController(t)
163 | defer ctrl.Finish()
164 | m := NewMockCloudInstances(ctrl) // skipcq: SCC-compile
165 |
166 | m.EXPECT().ListInstances().Return(instances, nil)
167 |
168 | assert.NoError(t, input.listInstances(m))
169 | // TODO test integractive part
170 | }
171 |
172 | // TestListSessions verifies listing sessions
173 | func TestListSessions(t *testing.T) {
174 | sessions := []*aws.Session{
175 | {
176 | SessionID: "test-1234567890",
177 | Target: "i-xxxxxxxxxxxxxxxx1",
178 | Status: "Connected",
179 | StartDate: "2019-05-03T10:08:44Z",
180 | Owner: "arn:aws:sts::0123456789:assumed-role/test/test",
181 | },
182 | }
183 | interactive := false
184 | format := FormatText
185 | input := StartInput{
186 | OutputFormat: &format,
187 | Interactive: &interactive,
188 | }
189 |
190 | ctrl := gomock.NewController(t)
191 | defer ctrl.Finish()
192 | m := NewMockCloudSessions(ctrl) // skipcq: SCC-compile
193 |
194 | m.EXPECT().ListSessions().Return(sessions, nil)
195 |
196 | assert.NoError(t, input.listSessions(m))
197 | // TODO test integractive part
198 | }
199 |
--------------------------------------------------------------------------------
/pkg/os/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library")
2 |
3 | go_library(
4 | name = "go_default_library",
5 | srcs = [
6 | "ignore_signals.go",
7 | "ignore_signals_windows.go",
8 | ],
9 | importpath = "github.com/danmx/sigil/pkg/os",
10 | visibility = ["//visibility:public"],
11 | )
12 |
--------------------------------------------------------------------------------
/pkg/os/ignore_signals.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package os
5 |
6 | import (
7 | "os"
8 | "os/signal"
9 | "syscall"
10 | )
11 |
12 | // IgnoreUserEnteredSignals ignores user signals
13 | func IgnoreUserEnteredSignals() {
14 | signals := []os.Signal{syscall.SIGINT, syscall.SIGSTOP, syscall.SIGTSTP, syscall.SIGQUIT}
15 | for _, s := range signals {
16 | signal.Ignore(s)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/os/ignore_signals_windows.go:
--------------------------------------------------------------------------------
1 | //go:build windows
2 | // +build windows
3 |
4 | package os
5 |
6 | import (
7 | "os"
8 | "os/signal"
9 | "syscall"
10 | )
11 |
12 | // IgnoreUserEnteredSignals ignores user signals
13 | func IgnoreUserEnteredSignals() {
14 | signals := []os.Signal{syscall.SIGINT}
15 | for _, s := range signals {
16 | signal.Ignore(s)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/pkg/session/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock")
3 |
4 | go_library(
5 | name = "go_default_library",
6 | srcs = ["session.go"],
7 | importpath = "github.com/danmx/sigil/pkg/session",
8 | visibility = ["//visibility:public"],
9 | deps = [
10 | "//pkg/aws:go_default_library",
11 | "@com_github_sirupsen_logrus//:go_default_library",
12 | ],
13 | )
14 |
15 | go_test(
16 | name = "go_default_test",
17 | srcs = [
18 | "aws_mock_test.go",
19 | "session_test.go",
20 | ],
21 | embed = [":go_default_library"],
22 | deps = [
23 | "//pkg/aws:go_default_library",
24 | "@com_github_golang_mock//gomock:go_default_library",
25 | "@com_github_stretchr_testify//assert:go_default_library",
26 | ],
27 | )
28 |
29 | gomock(
30 | name = "mock_aws",
31 | out = "aws_mock_test.go",
32 | interfaces = [
33 | "Cloud",
34 | "CloudInstances",
35 | "CloudSessions",
36 | "CloudSSH",
37 | ],
38 | library = "//pkg/aws:go_default_library",
39 | package = "session",
40 | self_package = "github.com/danmx/sigil/pkg/session",
41 | )
42 |
--------------------------------------------------------------------------------
/pkg/session/session.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | import (
4 | "github.com/danmx/sigil/pkg/aws"
5 |
6 | log "github.com/sirupsen/logrus"
7 | )
8 |
9 | // Session wraps methods used from the pkg/session package
10 | type Session interface {
11 | Start(input *StartInput) error
12 | }
13 |
14 | // StartInput struct contains all input data
15 | type StartInput struct {
16 | Target *string
17 | TargetType *string
18 | MFAToken *string
19 | Region *string
20 | Profile *string
21 | Trace *bool
22 | }
23 |
24 | // Start will start a session in chosen instance
25 | func Start(input *StartInput) error {
26 | return input.start(new(aws.Provider))
27 | }
28 |
29 | func (input *StartInput) start(provider aws.CloudInstances) error {
30 | err := provider.NewWithConfig(&aws.Config{
31 | Region: *input.Region,
32 | Profile: *input.Profile,
33 | MFAToken: *input.MFAToken,
34 | Trace: *input.Trace,
35 | })
36 | if err != nil {
37 | log.Error("Failed to generate new provider")
38 | return err
39 | }
40 | if err := provider.StartSession(*input.TargetType, *input.Target); err != nil {
41 | log.WithFields(log.Fields{
42 | "target": *input.Target,
43 | "targetType": *input.TargetType,
44 | }).Error("Failed to start a session")
45 | return err
46 | }
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/session/session_test.go:
--------------------------------------------------------------------------------
1 | package session
2 |
3 | // AWS Mock generation
4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/session -package session -destination aws_mock_test.go github.com/danmx/sigil/pkg/aws Cloud,CloudInstances,CloudSessions,CloudSSH
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/danmx/sigil/pkg/aws"
10 |
11 | "github.com/golang/mock/gomock"
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | // TestStart verifies start session method for different configurations
16 | func TestStart(t *testing.T) {
17 | ctrl := gomock.NewController(t)
18 | defer ctrl.Finish()
19 | m := NewMockCloudInstances(ctrl) // skipcq: SCC-compile
20 |
21 | mfa := "123456"
22 | region := "eu-west-1"
23 | profile := "west"
24 | trace := false
25 | input := StartInput{
26 | MFAToken: &mfa,
27 | Region: ®ion,
28 | Profile: &profile,
29 | Trace: &trace,
30 | }
31 | // Instance ID
32 | target := "i-xxxxxxxxxxxxxxxx1"
33 | targetType := aws.TargetTypeInstanceID
34 | input.Target = &target
35 | input.TargetType = &targetType
36 | gomock.InOrder(
37 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{
38 | Region: *input.Region,
39 | Profile: *input.Profile,
40 | MFAToken: *input.MFAToken,
41 | Trace: *input.Trace,
42 | })).Return(nil),
43 | m.EXPECT().StartSession(gomock.Eq(*input.TargetType), gomock.Eq(*input.Target)).Return(nil),
44 | )
45 | assert.NoError(t, input.start(m))
46 | // DNS
47 | target = "test.local"
48 | targetType = aws.TargetTypePrivateDNS
49 | gomock.InOrder(
50 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{
51 | Region: *input.Region,
52 | Profile: *input.Profile,
53 | MFAToken: *input.MFAToken,
54 | Trace: *input.Trace,
55 | })).Return(nil),
56 | m.EXPECT().StartSession(gomock.Eq(*input.TargetType), gomock.Eq(*input.Target)).Return(nil),
57 | )
58 | assert.NoError(t, input.start(m))
59 | // Name
60 | target = "Backend"
61 | targetType = aws.TargetTypeName
62 | gomock.InOrder(
63 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{
64 | Region: *input.Region,
65 | Profile: *input.Profile,
66 | MFAToken: *input.MFAToken,
67 | Trace: *input.Trace,
68 | })).Return(nil),
69 | m.EXPECT().StartSession(gomock.Eq(*input.TargetType), gomock.Eq(*input.Target)).Return(nil),
70 | )
71 | assert.NoError(t, input.start(m))
72 | }
73 |
--------------------------------------------------------------------------------
/pkg/ssh/BUILD.bazel:
--------------------------------------------------------------------------------
1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
2 | load("@com_github_jmhodges_bazel_gomock//:gomock.bzl", "gomock")
3 |
4 | go_library(
5 | name = "go_default_library",
6 | srcs = ["ssh.go"],
7 | importpath = "github.com/danmx/sigil/pkg/ssh",
8 | visibility = ["//visibility:public"],
9 | deps = [
10 | "//pkg/aws:go_default_library",
11 | "@com_github_sirupsen_logrus//:go_default_library",
12 | "@org_golang_x_crypto//ssh:go_default_library",
13 | ],
14 | )
15 |
16 | go_test(
17 | name = "go_default_test",
18 | srcs = [
19 | "aws_mock_test.go",
20 | "ssh_test.go",
21 | ],
22 | embed = [":go_default_library"],
23 | deps = [
24 | "//pkg/aws:go_default_library",
25 | "@com_github_golang_mock//gomock:go_default_library",
26 | "@com_github_stretchr_testify//assert:go_default_library",
27 | ],
28 | )
29 |
30 | gomock(
31 | name = "mock_aws",
32 | out = "aws_mock_test.go",
33 | interfaces = [
34 | "Cloud",
35 | "CloudInstances",
36 | "CloudSessions",
37 | "CloudSSH",
38 | ],
39 | library = "//pkg/aws:go_default_library",
40 | package = "ssh",
41 | self_package = "github.com/danmx/sigil/pkg/ssh",
42 | )
43 |
--------------------------------------------------------------------------------
/pkg/ssh/ssh.go:
--------------------------------------------------------------------------------
1 | package ssh
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/rsa"
6 | "crypto/x509"
7 | "encoding/pem"
8 | "io/ioutil"
9 | "os"
10 | "strings"
11 |
12 | "github.com/danmx/sigil/pkg/aws"
13 |
14 | log "github.com/sirupsen/logrus"
15 | "golang.org/x/crypto/ssh"
16 | )
17 |
18 | // SSH wraps methods used from the pkg/ssh package
19 | type SSH interface {
20 | Start(input *StartInput) error
21 | }
22 |
23 | // StartInput struct contains all input data
24 | type StartInput struct {
25 | Target *string
26 | TargetType *string
27 | PortNumber *uint64
28 | PublicKey *string
29 | OSUser *string
30 | GenKeyPair *bool
31 | MFAToken *string
32 | Region *string
33 | Profile *string
34 | Trace *bool
35 | }
36 |
37 | // Start will start ssh session
38 | func Start(input *StartInput) error {
39 | return input.start(new(aws.Provider))
40 | }
41 |
42 | func (input *StartInput) start(provider aws.CloudSSH) error {
43 | err := provider.NewWithConfig(&aws.Config{
44 | Region: *input.Region,
45 | Profile: *input.Profile,
46 | MFAToken: *input.MFAToken,
47 | Trace: *input.Trace,
48 | })
49 | if err != nil {
50 | log.Error(err)
51 | return err
52 | }
53 | pubKey := *input.PublicKey
54 | if *input.GenKeyPair {
55 | const rsaKeySize = 4092
56 | privKeyBlob, errKey := rsa.GenerateKey(rand.Reader, rsaKeySize)
57 | if errKey != nil {
58 | return errKey
59 | }
60 | pubKeyBlob := privKeyBlob.PublicKey
61 | if errPubPEM := savePublicPEMKey(pubKey, &pubKeyBlob); errPubPEM != nil {
62 | return errPubPEM
63 | }
64 | defer func() {
65 | if err = deleteTempKey(pubKey); err != nil {
66 | log.Error(err)
67 | }
68 | }()
69 | privKey := strings.TrimSuffix(pubKey, ".pub")
70 | if errPrivPEM := savePrivPEMKey(privKey, privKeyBlob); errPrivPEM != nil {
71 | return errPrivPEM
72 | }
73 | defer func() {
74 | if err = deleteTempKey(privKey); err != nil {
75 | log.Error(err)
76 | }
77 | }()
78 | }
79 |
80 | pubKeyData := []byte{}
81 | if pubKey != "" {
82 | pubKeyData, err = ioutil.ReadFile(pubKey)
83 | if err != nil {
84 | return err
85 | }
86 | }
87 |
88 | log.WithFields(log.Fields{
89 | "targetType": *input.TargetType,
90 | "PortNumber": *input.PortNumber,
91 | "target": *input.Target,
92 | "OSUser": *input.OSUser,
93 | "pubKeyData": string(pubKeyData),
94 | "PublicKeyPath": pubKey,
95 | }).Debug("StartSSHInput")
96 |
97 | // returns err
98 | return provider.StartSSH(*input.TargetType, *input.Target, *input.OSUser, *input.PortNumber, pubKeyData)
99 | }
100 |
101 | // Helper functions
102 |
103 | func savePrivPEMKey(fileName string, key *rsa.PrivateKey) error {
104 | var privateKey = &pem.Block{
105 | Type: "RSA PRIVATE KEY",
106 | Headers: nil,
107 | Bytes: x509.MarshalPKCS1PrivateKey(key),
108 | }
109 |
110 | // returns err
111 | return ioutil.WriteFile(fileName, pem.EncodeToMemory(privateKey), 0600) //nolint:gomnd // Linux file permissions
112 | }
113 |
114 | func savePublicPEMKey(fileName string, pubkey *rsa.PublicKey) error {
115 | pub, err := ssh.NewPublicKey(pubkey)
116 | if err != nil {
117 | return err
118 | }
119 | // returns err
120 | return ioutil.WriteFile(fileName, ssh.MarshalAuthorizedKey(pub), 0600) //nolint:gomnd // Linux file permissions
121 | }
122 |
123 | func deleteTempKey(keyPath string) error {
124 | stat, err := os.Stat(keyPath)
125 | log.WithFields(log.Fields{
126 | "stat": stat,
127 | "err": err,
128 | }).Debug("Checking if key exist")
129 | if err == nil {
130 | if errRm := os.Remove(keyPath); errRm != nil {
131 | return errRm
132 | }
133 | }
134 | return err
135 | }
136 |
--------------------------------------------------------------------------------
/pkg/ssh/ssh_test.go:
--------------------------------------------------------------------------------
1 | package ssh
2 |
3 | // AWS Mock generation
4 | //go:generate go run github.com/golang/mock/mockgen -self_package=github.com/danmx/sigil/pkg/ssh -package ssh -destination aws_mock_test.go github.com/danmx/sigil/pkg/aws Cloud,CloudInstances,CloudSessions,CloudSSH
5 |
6 | import (
7 | "os"
8 | "path"
9 | "testing"
10 |
11 | "github.com/danmx/sigil/pkg/aws"
12 |
13 | "github.com/golang/mock/gomock"
14 | "github.com/stretchr/testify/assert"
15 | )
16 |
17 | // TestStart verifies start ssh method
18 | func TestStart(t *testing.T) {
19 | // Instance
20 | ctrl := gomock.NewController(t)
21 | defer ctrl.Finish()
22 | m := NewMockCloudSSH(ctrl) // skipcq: SCC-compile
23 |
24 | target := "i-xxxxxxxxxxxxxxxx1"
25 | targetType := aws.TargetTypeInstanceID
26 | mfa := "123456"
27 | region := "eu-west-1"
28 | profile := "west"
29 | var port uint64 = 22
30 | pubKey := path.Join(os.TempDir(), "sigil_test.pub")
31 | osUser := "ec2-user"
32 | genKey := true
33 | trace := false
34 | input := StartInput{
35 | MFAToken: &mfa,
36 | Region: ®ion,
37 | Profile: &profile,
38 | Target: &target,
39 | TargetType: &targetType,
40 | PortNumber: &port,
41 | PublicKey: &pubKey,
42 | OSUser: &osUser,
43 | GenKeyPair: &genKey,
44 | Trace: &trace,
45 | }
46 |
47 | gomock.InOrder(
48 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{
49 | Region: *input.Region,
50 | Profile: *input.Profile,
51 | MFAToken: *input.MFAToken,
52 | Trace: *input.Trace,
53 | })).Return(nil),
54 | m.EXPECT().StartSSH(
55 | gomock.Eq(*input.TargetType),
56 | gomock.Eq(*input.Target),
57 | gomock.Eq(*input.OSUser),
58 | gomock.Eq(*input.PortNumber),
59 | gomock.Any(),
60 | ).Return(nil),
61 | )
62 |
63 | assert.NoError(t, input.start(m))
64 |
65 | // DNS
66 | target = "test.local"
67 | targetType = aws.TargetTypePrivateDNS
68 | input = StartInput{
69 | MFAToken: &mfa,
70 | Region: ®ion,
71 | Profile: &profile,
72 | Target: &target,
73 | TargetType: &targetType,
74 | PortNumber: &port,
75 | PublicKey: &pubKey,
76 | OSUser: &osUser,
77 | GenKeyPair: &genKey,
78 | Trace: &trace,
79 | }
80 |
81 | gomock.InOrder(
82 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{
83 | Region: *input.Region,
84 | Profile: *input.Profile,
85 | MFAToken: *input.MFAToken,
86 | Trace: *input.Trace,
87 | })).Return(nil),
88 | m.EXPECT().StartSSH(
89 | gomock.Eq(*input.TargetType),
90 | gomock.Eq(*input.Target),
91 | gomock.Eq(*input.OSUser),
92 | gomock.Eq(*input.PortNumber),
93 | gomock.Any(),
94 | ).Return(nil),
95 | )
96 |
97 | assert.NoError(t, input.start(m))
98 |
99 | // Name
100 | target = "Backend"
101 | targetType = aws.TargetTypePrivateDNS
102 | input = StartInput{
103 | MFAToken: &mfa,
104 | Region: ®ion,
105 | Profile: &profile,
106 | Target: &target,
107 | TargetType: &targetType,
108 | PortNumber: &port,
109 | PublicKey: &pubKey,
110 | OSUser: &osUser,
111 | GenKeyPair: &genKey,
112 | Trace: &trace,
113 | }
114 |
115 | gomock.InOrder(
116 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{
117 | Region: *input.Region,
118 | Profile: *input.Profile,
119 | MFAToken: *input.MFAToken,
120 | Trace: *input.Trace,
121 | })).Return(nil),
122 | m.EXPECT().StartSSH(
123 | gomock.Eq(*input.TargetType),
124 | gomock.Eq(*input.Target),
125 | gomock.Eq(*input.OSUser),
126 | gomock.Eq(*input.PortNumber),
127 | gomock.Any(),
128 | ).Return(nil),
129 | )
130 |
131 | assert.NoError(t, input.start(m))
132 |
133 | gomock.InOrder(
134 | m.EXPECT().NewWithConfig(gomock.Eq(&aws.Config{
135 | Region: *input.Region,
136 | Profile: *input.Profile,
137 | MFAToken: *input.MFAToken,
138 | Trace: *input.Trace,
139 | })).Return(nil),
140 | m.EXPECT().StartSSH(
141 | gomock.Eq(*input.TargetType),
142 | gomock.Eq(*input.Target),
143 | gomock.Eq(*input.OSUser),
144 | gomock.Eq(*input.PortNumber),
145 | gomock.Any(),
146 | ).Return(nil),
147 | )
148 |
149 | assert.NoError(t, input.start(m))
150 | }
151 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ],
5 | "schedule": "at 7am on Monday"
6 | }
7 |
--------------------------------------------------------------------------------
/scripts/pre-commit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -eu
4 |
5 | test(){
6 | bazel test //...
7 | }
8 |
9 | fmt() {
10 | bazel run :gazelle && bazel run @go_sdk//:bin/gofmt -- -s -w .
11 | }
12 |
13 | lint() {
14 | bazel run @com_github_danmx_bazel_tools//golangci-lint:run -- run ./...
15 | }
16 |
17 | update_deps() {
18 | bazel run @go_sdk//:bin/go -- mod tidy && bazelisk run :gazelle -- update-repos -from_file=go.mod -to_macro=tools/repositories.bzl%go_repositories -prune=true
19 | }
20 |
21 | changelog(){
22 | bazel run @com_github_danmx_bazel_tools//git-chglog:run -- -o CHANGELOG.md
23 | }
24 |
25 | #shellcheck disable=SC2068
26 | $@
27 |
--------------------------------------------------------------------------------
/tools/BUILD:
--------------------------------------------------------------------------------
1 | package(default_visibility = ["//visibility:public"])
2 |
3 | exports_files(["fix_codecov.sh"])
4 |
--------------------------------------------------------------------------------
/tools/fix_codecov.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | rootDir="bazel-out"
6 |
7 | echo "mode: set"
8 |
9 | while IFS= read -r -d '' i
10 | do
11 | # >&2 echo "${i} -> ${i//.dat/.txt}"
12 | >&2 echo "parsing: ${i}"
13 | # cp "${i}" "${i//.dat/.txt}"
14 | tail -n +2 "${i}"
15 | done < <(find "${rootDir}"/ -name coverage.dat -print0)
16 |
--------------------------------------------------------------------------------
/tools/get_workspace_status.sh:
--------------------------------------------------------------------------------
1 | #! /bin/sh
2 |
3 | set -eu
4 |
5 | appName="sigil"
6 | appVersion="${VERSION:-0.7.0}"
7 | gitCommit="${GIT_COMMIT:-$(git rev-parse HEAD)}"
8 | gitBranch="${GIT_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
9 |
10 | IFS='.' read -r major minor patch << EOF
11 | ${appVersion}
12 | EOF
13 |
14 | cat <