├── .github └── workflows │ ├── ci-only.yaml │ ├── release.yaml │ └── security-analysis.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── cmd ├── boundary_init_db.go ├── boundary_install.go ├── command.go ├── consul_install.go ├── get.go ├── nomad_install.go ├── service.go ├── target.go ├── tls.go ├── uninstall.go ├── vault_install.go └── version.go ├── docs ├── boundary.md ├── consul.md ├── nomad.md └── vault.md ├── examples ├── digitalocean │ ├── install_boundary.sh │ ├── main.tf │ ├── scripts │ │ ├── setup.sh │ │ └── setup_db.sh │ ├── variables.tf │ └── versions.tf ├── multipass │ ├── consul │ │ ├── destroy.sh │ │ └── start.sh │ ├── nomad │ │ ├── destroy.sh │ │ └── start.sh │ └── vault │ │ ├── destroy.sh │ │ └── start.sh └── vagrant │ ├── Vagrantfile │ ├── client.sh │ └── server.sh ├── go.mod ├── go.sum ├── main.go ├── pkg ├── archive │ └── unzip.go ├── config │ ├── boundary.go │ ├── consul.go │ ├── functions.go │ ├── nomad.go │ ├── vault.go │ └── version.go └── operator │ ├── errors.go │ ├── local.go │ ├── operator.go │ └── ssh.go └── scripts ├── install_boundary.sh ├── install_boundary_db.sh ├── install_consul.sh ├── install_nomad.sh ├── install_vault.sh ├── scripts.go ├── service.sh └── uninstall.sh /.github/workflows/ci-only.yaml: -------------------------------------------------------------------------------- 1 | name: ci-only 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/setup-go@v2 14 | with: 15 | go-version: 1.19 16 | 17 | - uses: actions/checkout@v2.3.4 18 | with: 19 | fetch-depth: 1 20 | 21 | - uses: actions/cache@v2 22 | with: 23 | path: ~/go/pkg/mod 24 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 25 | restore-keys: | 26 | ${{ runner.os }}-go- 27 | 28 | - run: go build 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | id-token: write 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: 1.19 26 | 27 | - name: Install cosign 28 | uses: sigstore/cosign-installer@v2.5.1 29 | 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v2 32 | with: 33 | version: latest 34 | args: release --rm-dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/security-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Security Analysis" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | codeql: 10 | name: CodeQL 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: read 14 | contents: read 15 | security-events: write 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: Initialize CodeQL 22 | uses: github/codeql-action/init@v1 23 | with: 24 | languages: go 25 | - name: Perform CodeQL Analysis 26 | uses: github/codeql-action/analyze@v1 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vagrant 3 | dist 4 | 5 | hashi-up 6 | pkged.go 7 | 8 | .terraform 9 | *.tfstate 10 | *.tfstate.backup 11 | *.tfvars -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: hashi-up 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - id: default 9 | env: [ CGO_ENABLED=0 ] 10 | goos: 11 | - linux 12 | - windows 13 | goarch: 14 | - amd64 15 | ldflags: 16 | - -s -w -X github.com/jsiebens/hashi-up/cmd.Version={{.Version}} -X github.com/jsiebens/hashi-up/cmd.GitCommit={{.ShortCommit}} 17 | - id: arm 18 | env: [ CGO_ENABLED=0 ] 19 | goos: 20 | - linux 21 | goarch: 22 | - arm64 23 | - arm 24 | ldflags: 25 | - -s -w -X github.com/jsiebens/hashi-up/cmd.Version={{.Version}} -X github.com/jsiebens/hashi-up/cmd.GitCommit={{.ShortCommit}} 26 | - id: darwin 27 | env: [ CGO_ENABLED=0 ] 28 | goos: 29 | - darwin 30 | goarch: 31 | - amd64 32 | ldflags: 33 | - -s -w -X github.com/jsiebens/hashi-up/cmd.Version={{.Version}} -X github.com/jsiebens/hashi-up/cmd.GitCommit={{.ShortCommit}} 34 | 35 | archives: 36 | - id: default 37 | builds: 38 | - default 39 | format: binary 40 | name_template: "{{ .ProjectName }}" 41 | - id: darwin 42 | builds: 43 | - darwin 44 | format: binary 45 | name_template: "{{ .ProjectName }}-{{ .Os }}" 46 | - id: arm 47 | builds: 48 | - arm 49 | format: binary 50 | name_template: "{{ .ProjectName }}-{{ .Arch }}{{ if .Arm }}hf{{ end }}" 51 | 52 | checksum: 53 | name_template: "checksums.txt" 54 | 55 | signs: 56 | - cmd: cosign 57 | env: 58 | - COSIGN_EXPERIMENTAL=1 59 | certificate: '${artifact}.pem' 60 | args: 61 | - sign-blob 62 | - '--output-certificate=${certificate}' 63 | - '--output-signature=${signature}' 64 | - '${artifact}' 65 | artifacts: checksum 66 | 67 | changelog: 68 | sort: asc 69 | filters: 70 | exclude: 71 | - '^test:' 72 | - '^chore' 73 | - '^docs' 74 | - Merge pull request 75 | - Merge remote-tracking branch 76 | - Merge branch 77 | - go mod tidy 78 | groups: 79 | - title: 'New Features' 80 | regexp: "^.*feat[(\\w)]*:+.*$" 81 | order: 0 82 | - title: 'Bug fixes' 83 | regexp: "^.*fix[(\\w)]*:+.*$" 84 | order: 10 85 | - title: Other work 86 | order: 999 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Johan Siebens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hashi-up 2 | 3 | hashi-up is a lightweight utility to install HashiCorp [Consul](https://www.consul.io/), [Nomad](https://www.nomadproject.io) or [Vault](https://www.vaultproject.io/) on any remote Linux host. All you need is `ssh` access and the binary `hashi-up` to build a Consul, Nomad or Vault cluster. 4 | 5 | The tool is written in Go and is cross-compiled for Linux, Windows, MacOS and even on Raspberry Pi. 6 | 7 | This project is heavily inspired on the work of [Alex Ellis](https://www.alexellis.io/) who created [k3sup](https://k3sup.dev/), a tool to to get from zero to KUBECONFIG with [k3s](https://k3s.io/) 8 | 9 | [![Go Report Card](https://goreportcard.com/badge/github.com/jsiebens/hashi-up)](https://goreportcard.com/report/github.com/jsiebens/hashi-up) 10 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 11 | ![GitHub All Releases](https://img.shields.io/github/downloads/jsiebens/hashi-up/total) 12 | 13 | ## What's this for? 14 | 15 | This tool uses `ssh` to install HashiCorp Consul, Nomad or Vault to a remote Linux host. You can also use it to join existing Linux hosts into a Consul, Nomad, Vault or Boundary cluster. First, Consul, Nomad or Vault is installed using a utility script, along with a minimal configuration to run the agent as server or client. 16 | 17 | `hashi-up` was developed to automate what can be a very manual and confusing process for many developers, who are already short on time. Once you've provisioned a VM with your favourite tooling, `hashi-up` means you are only 60 seconds away from running `nomad status` on your own computer. 18 | 19 | ## Download `hashi-up` 20 | 21 | `hashi-up` is distributed as a static Go binary. 22 | You can use the installer on MacOS and Linux, or visit the Releases page to download the executable for Windows. 23 | 24 | ``` shell 25 | curl -sLS https://get.hashi-up.dev | sh 26 | sudo install hashi-up /usr/local/bin/ 27 | 28 | hashi-up version 29 | ``` 30 | 31 | ## Usage 32 | 33 | The `hashi-up` tool is a client application which you can run on your own computer. It uses SSH to connect to remote servers when installing HashiCorp Consul or Nomad. Binaries are provided for MacOS, Windows, and Linux (including ARM). 34 | 35 | ### SSH credentials 36 | 37 | By default, `hashi-up` talks to an SSH agent on your host via the SSH agent protocol. This saves you from typing a passphrase for an encrypted private key every time you connect to a server. 38 | The `ssh-agent` that comes with OpenSSH is commonly used, but other agents, like gpg-agent or yubikey-agent are supported by setting the `SSH_AUTH_SOCK` environment variable to the Unix domain socket of the agent. 39 | 40 | The `--ssh-target-key` flag can be used when no agent is available or when a specific private key is preferred. 41 | 42 | The `--ssh-target-user` and `--ssh-target-password` flags allow you to authenticate using a username and a password. 43 | 44 | ### Guides 45 | 46 | - [Installing Consul](docs/consul.md) 47 | - [Installing Nomad](docs/nomad.md) 48 | - [Installing Vault](docs/vault.md) 49 | - [Installing Boundary](docs/boundary.md) 50 | 51 | ## Resources 52 | 53 | [Deploying a highly-available Nomad cluster with hashi-up!](https://johansiebens.dev/posts/2020/07/deploying-a-highly-available-nomad-cluster-with-hashi-up/) 54 | 55 | [Building a Nomad cluster on Raspberry Pi running Ubuntu server](https://johansiebens.dev/posts/2020/08/building-a-nomad-cluster-on-raspberry-pi-running-ubuntu-server/) 56 | 57 | [Installing HashiCorp Vault on DigitalOcean with hashi-up](https://johansiebens.dev/posts/2020/12/installing-hashicorp-vault-on-digitalocean-with-hashi-up/) 58 | -------------------------------------------------------------------------------- /cmd/boundary_init_db.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/jsiebens/hashi-up/pkg/config" 8 | "github.com/jsiebens/hashi-up/pkg/operator" 9 | "github.com/jsiebens/hashi-up/scripts" 10 | "github.com/muesli/coral" 11 | "github.com/pkg/errors" 12 | "github.com/thanhpk/randstr" 13 | ) 14 | 15 | func InitBoundaryDatabaseCommand() *coral.Command { 16 | 17 | var binary string 18 | var version string 19 | 20 | var configFile string 21 | 22 | var flags = config.BoundaryConfig{} 23 | 24 | var command = &coral.Command{ 25 | Use: "init-database", 26 | SilenceUsage: true, 27 | } 28 | 29 | var target = Target{} 30 | target.prepareCommand(command) 31 | 32 | command.Flags().StringVar(&binary, "package", "", "Upload and use this Boundary package instead of downloading") 33 | command.Flags().StringVarP(&version, "version", "v", "", "Version of Boundary to install") 34 | 35 | command.Flags().StringVarP(&configFile, "config-file", "c", "", "Custom Boundary configuration file to upload") 36 | 37 | command.Flags().StringVar(&flags.DatabaseURL, "db-url", "", "Boundary: configures the URL for connecting to Postgres") 38 | command.Flags().StringVar(&flags.RootKey, "root-key", "", "Boundary: a KEK (Key Encrypting Key) for the scope-specific KEKs (also referred to as the scope's root key).") 39 | 40 | command.RunE = func(command *coral.Command, args []string) error { 41 | if !target.Local && len(target.Addr) == 0 { 42 | return fmt.Errorf("required ssh-target-addr flag is missing") 43 | } 44 | 45 | ignoreConfigFlags := len(configFile) != 0 46 | 47 | var generatedConfig string 48 | 49 | if !ignoreConfigFlags { 50 | if !flags.HasDatabaseURL() { 51 | return fmt.Errorf("a db-url is required when initializing the database") 52 | } 53 | if !flags.HasRootKey() { 54 | return fmt.Errorf("a root-key when initializing the database") 55 | } 56 | 57 | generatedConfig = flags.GenerateDbConfigFile() 58 | } 59 | 60 | if len(binary) == 0 && len(version) == 0 { 61 | latest, err := config.GetLatestVersion("boundary") 62 | 63 | if err != nil { 64 | return errors.Wrapf(err, "unable to get latest version number, define a version manually with the --version flag") 65 | } 66 | 67 | version = latest 68 | } 69 | 70 | callback := func(op operator.CommandOperator) error { 71 | dir := "/tmp/hashi-up." + randstr.String(6) 72 | 73 | defer op.Execute("rm -rf " + dir) 74 | 75 | err := op.Execute("mkdir -p " + dir + "/config") 76 | if err != nil { 77 | return fmt.Errorf("error received during installation: %s", err) 78 | } 79 | 80 | if len(binary) != 0 { 81 | info("Uploading Boundary package ...") 82 | err = op.UploadFile(binary, dir+"/boundary.zip", "0640") 83 | if err != nil { 84 | return fmt.Errorf("error received during upload Boundary package: %s", err) 85 | } 86 | } 87 | 88 | if !ignoreConfigFlags { 89 | info("Uploading generated Boundary configuration ...") 90 | err = op.Upload(strings.NewReader(generatedConfig), dir+"/config/boundary.hcl", "0640") 91 | if err != nil { 92 | return fmt.Errorf("error received during upload boundary configuration: %s", err) 93 | } 94 | } else { 95 | info(fmt.Sprintf("Uploading %s as boundary.hcl...", configFile)) 96 | err = op.UploadFile(expandPath(configFile), dir+"/config/boundary.hcl", "0640") 97 | if err != nil { 98 | return fmt.Errorf("error received during upload boundary configuration: %s", err) 99 | } 100 | } 101 | 102 | data := map[string]interface{}{ 103 | "TmpDir": dir, 104 | "Version": version, 105 | } 106 | 107 | installScript, err := scripts.RenderScript("install_boundary_db.sh", data) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | err = op.Upload(installScript, dir+"/install.sh", "0755") 113 | if err != nil { 114 | return fmt.Errorf("error received during upload install script: %s", err) 115 | } 116 | 117 | info("Initializing Boundary database ...") 118 | sudoPass, err := target.sudoPass() 119 | if err != nil { 120 | return fmt.Errorf("error received during installation: %s", err) 121 | } 122 | err = op.Execute(fmt.Sprintf("cat %s/install.sh | SUDO_PASS=\"%s\" sh -\n", dir, sudoPass)) 123 | if err != nil { 124 | return fmt.Errorf("error received during installation: %s", err) 125 | } 126 | 127 | info("Done.") 128 | 129 | return nil 130 | } 131 | 132 | return target.execute(callback) 133 | } 134 | 135 | return command 136 | } 137 | -------------------------------------------------------------------------------- /cmd/boundary_install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/jsiebens/hashi-up/pkg/config" 9 | "github.com/jsiebens/hashi-up/pkg/operator" 10 | "github.com/jsiebens/hashi-up/scripts" 11 | "github.com/muesli/coral" 12 | "github.com/pkg/errors" 13 | "github.com/thanhpk/randstr" 14 | ) 15 | 16 | func InstallBoundaryCommand() *coral.Command { 17 | 18 | var skipConfig bool 19 | var skipEnable bool 20 | var skipStart bool 21 | var binary string 22 | var version string 23 | 24 | var configFile string 25 | var files []string 26 | 27 | var flags = config.BoundaryConfig{} 28 | 29 | var command = &coral.Command{ 30 | Use: "install", 31 | Short: "Install Boundary on a server via SSH", 32 | Long: "Install Boundary on a server via SSH", 33 | SilenceUsage: true, 34 | } 35 | 36 | var target = Target{} 37 | target.prepareCommand(command) 38 | 39 | command.Flags().BoolVar(&skipConfig, "skip-config", false, "If set to true will install Boundary service without touching existing config files") 40 | command.Flags().BoolVar(&skipEnable, "skip-enable", false, "If set to true will not enable or start Boundary service") 41 | command.Flags().BoolVar(&skipStart, "skip-start", false, "If set to true will not start Boundary service") 42 | command.Flags().StringVar(&binary, "package", "", "Upload and use this Boundary package instead of downloading") 43 | command.Flags().StringVarP(&version, "version", "v", "", "Version of Boundary to install") 44 | 45 | command.Flags().StringVarP(&configFile, "config-file", "c", "", "Custom Boundary configuration file to upload") 46 | command.Flags().StringArrayVarP(&files, "file", "f", []string{}, "Additional files, e.g. certificates, to upload") 47 | 48 | command.Flags().StringVar(&flags.ControllerName, "controller-name", "", "Boundary: specifies a unique name of this controller within the Boundary controller cluster.") 49 | command.Flags().StringVar(&flags.WorkerName, "worker-name", "", "Boundary: specifies a unique name of this worker within the Boundary worker cluster.") 50 | command.Flags().StringVar(&flags.DatabaseURL, "db-url", "", "Boundary: configures the URL for connecting to Postgres") 51 | command.Flags().StringVar(&flags.RootKey, "root-key", "", "Boundary: a KEK (Key Encrypting Key) for the scope-specific KEKs (also referred to as the scope's root key).") 52 | command.Flags().StringVar(&flags.WorkerAuthKey, "worker-auth-key", "", "Boundary: KMS key shared by the Controller and Worker in order to authenticate a Worker to the Controller.") 53 | command.Flags().StringVar(&flags.RecoveryKey, "recovery-key", "", "Boundary: KMS key is used for rescue/recovery operations that can be used by a client to authenticate almost any operation within Boundary.") 54 | command.Flags().StringVar(&flags.ApiAddress, "api-addr", "0.0.0.0", "Boundary: address for the API listener") 55 | command.Flags().StringVar(&flags.ApiKeyFile, "api-key-file", "", "Boundary: specifies the path to the private key for the certificate.") 56 | command.Flags().StringVar(&flags.ApiCertFile, "api-cert-file", "", "Boundary: specifies the path to the certificate for TLS.") 57 | command.Flags().StringVar(&flags.ClusterAddress, "cluster-addr", "127.0.0.1", "Boundary: address for the Cluster listener") 58 | command.Flags().StringVar(&flags.ClusterKeyFile, "cluster-key-file", "", "Boundary: specifies the path to the private key for the certificate.") 59 | command.Flags().StringVar(&flags.ClusterCertFile, "cluster-cert-file", "", "Boundary: specifies the path to the certificate for TLS.") 60 | command.Flags().StringVar(&flags.ProxyAddress, "proxy-addr", "0.0.0.0", "Boundary: address for the Proxy listener") 61 | command.Flags().StringVar(&flags.ProxyKeyFile, "proxy-key-file", "", "Boundary: specifies the path to the private key for the certificate.") 62 | command.Flags().StringVar(&flags.ProxyCertFile, "proxy-cert-file", "", "Boundary: specifies the path to the certificate for TLS.") 63 | command.Flags().StringVar(&flags.PublicClusterAddress, "public-cluster-addr", "", "Boundary: specifies the public host or IP address (and optionally port) at which the controller can be reached by workers.") 64 | command.Flags().StringVar(&flags.PublicAddress, "public-addr", "", "Boundary: specifies the public host or IP address (and optionally port) at which the worker can be reached by clients for proxying.") 65 | command.Flags().StringArrayVar(&flags.Controllers, "controller", []string{"127.0.0.1"}, "Boundary: a list of hosts/IP addresses and optionally ports for reaching controllers.") 66 | 67 | command.RunE = func(command *coral.Command, args []string) error { 68 | if !target.Local && len(target.Addr) == 0 { 69 | return fmt.Errorf("required ssh-target-addr flag is missing") 70 | } 71 | 72 | ignoreConfigFlags := skipConfig || len(configFile) != 0 73 | 74 | var generatedConfig string 75 | 76 | if !ignoreConfigFlags { 77 | if !(flags.IsControllerEnabled() || flags.IsWorkerEnabled()) { 78 | return fmt.Errorf("a controller-name and/or a worker-name is required") 79 | } 80 | 81 | if flags.IsControllerEnabled() { 82 | if !flags.HasDatabaseURL() { 83 | return fmt.Errorf("a db-url is required when running a controller") 84 | } 85 | if !flags.HasAllRequiredControllerKeys() { 86 | return fmt.Errorf("a root-key, a worker-auth-key and a recovery-key are required when running a controller") 87 | } 88 | } 89 | 90 | if flags.IsWorkerEnabled() && !flags.HasAllRequiredWorkerKeys() { 91 | return fmt.Errorf("a worker-auth-key are required when running a worker") 92 | } 93 | 94 | if !flags.HasValidApiTLSSettings() { 95 | return fmt.Errorf("both api-key-file and api-cert-file are required to enable API TLS") 96 | } 97 | 98 | if !flags.HasValidClusterTLSSettings() { 99 | return fmt.Errorf("both cluster-key-file and cluster-cert-file are required to enable cluster TLS") 100 | } 101 | 102 | if !flags.HasValidProxyTLSSettings() { 103 | return fmt.Errorf("both proxy-key-file and proxy-cert-file are required to enable proxy TLS") 104 | } 105 | 106 | generatedConfig = flags.GenerateConfigFile() 107 | } 108 | 109 | if len(binary) == 0 && len(version) == 0 { 110 | latest, err := config.GetLatestVersion("boundary") 111 | 112 | if err != nil { 113 | return errors.Wrapf(err, "unable to get latest version number, define a version manually with the --version flag") 114 | } 115 | 116 | version = latest 117 | } 118 | 119 | callback := func(op operator.CommandOperator) error { 120 | dir := "/tmp/hashi-up." + randstr.String(6) 121 | 122 | defer op.Execute("rm -rf " + dir) 123 | 124 | err := op.Execute("mkdir -p " + dir + "/config") 125 | if err != nil { 126 | return fmt.Errorf("error received during installation: %s", err) 127 | } 128 | 129 | if len(binary) != 0 { 130 | info("Uploading Boundary package ...") 131 | err = op.UploadFile(binary, dir+"/boundary.zip", "0640") 132 | if err != nil { 133 | return fmt.Errorf("error received during upload Boundary package: %s", err) 134 | } 135 | } 136 | 137 | if !skipConfig { 138 | if !ignoreConfigFlags { 139 | info("Uploading generated Boundary configuration ...") 140 | err = op.Upload(strings.NewReader(generatedConfig), dir+"/config/boundary.hcl", "0640") 141 | if err != nil { 142 | return fmt.Errorf("error received during upload boundary configuration: %s", err) 143 | } 144 | 145 | files = []string{} 146 | 147 | if flags.ApiTLSEnabled() { 148 | files = []string{flags.ApiCertFile, flags.ApiKeyFile} 149 | } 150 | if flags.ClusterTLSEnabled() { 151 | files = []string{flags.ClusterKeyFile, flags.ClusterCertFile} 152 | } 153 | if flags.ProxyTLSEnabled() { 154 | files = []string{flags.ProxyKeyFile, flags.ProxyCertFile} 155 | } 156 | } else { 157 | info(fmt.Sprintf("Uploading %s as boundary.hcl...", configFile)) 158 | err = op.UploadFile(expandPath(configFile), dir+"/config/boundary.hcl", "0640") 159 | if err != nil { 160 | return fmt.Errorf("error received during upload boundary configuration: %s", err) 161 | } 162 | } 163 | 164 | for _, s := range files { 165 | if len(s) != 0 { 166 | info(fmt.Sprintf("Uploading %s...", s)) 167 | _, filename := filepath.Split(expandPath(s)) 168 | err = op.UploadFile(expandPath(s), dir+"/config/"+filename, "0640") 169 | if err != nil { 170 | return fmt.Errorf("error received during upload file: %s", err) 171 | } 172 | } 173 | } 174 | } 175 | 176 | data := map[string]interface{}{ 177 | "TmpDir": dir, 178 | "SkipEnable": skipEnable, 179 | "SkipStart": skipStart, 180 | "Version": version, 181 | } 182 | 183 | installScript, err := scripts.RenderScript("install_boundary.sh", data) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | err = op.Upload(installScript, dir+"/install.sh", "0755") 189 | if err != nil { 190 | return fmt.Errorf("error received during upload install script: %s", err) 191 | } 192 | 193 | info("Installing Boundary ...") 194 | sudoPass, err := target.sudoPass() 195 | if err != nil { 196 | return fmt.Errorf("error received during installation: %s", err) 197 | } 198 | err = op.Execute(fmt.Sprintf("cat %s/install.sh | SUDO_PASS=\"%s\" sh -\n", dir, sudoPass)) 199 | if err != nil { 200 | return fmt.Errorf("error received during installation: %s", err) 201 | } 202 | 203 | info("Done.") 204 | 205 | return nil 206 | } 207 | 208 | return target.execute(callback) 209 | } 210 | 211 | return command 212 | } 213 | -------------------------------------------------------------------------------- /cmd/command.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/mitchellh/go-homedir" 8 | "github.com/muesli/coral" 9 | ) 10 | 11 | type Installer func() *coral.Command 12 | 13 | func Execute() error { 14 | 15 | rootCmd := baseCommand("hashi-up") 16 | rootCmd.AddCommand(TlsCommands()) 17 | rootCmd.AddCommand(VersionCommand()) 18 | rootCmd.AddCommand(productCommand("consul", InstallConsulCommand)) 19 | rootCmd.AddCommand(productCommand("nomad", InstallNomadCommand)) 20 | rootCmd.AddCommand(productCommand("vault", InstallVaultCommand)) 21 | rootCmd.AddCommand(productCommand("boundary", InstallBoundaryCommand, InitBoundaryDatabaseCommand)) 22 | rootCmd.AddCommand(productCommand("terraform")) 23 | rootCmd.AddCommand(productCommand("packer")) 24 | rootCmd.AddCommand(productCommand("vagrant")) 25 | rootCmd.AddCommand(productCommand("waypoint")) 26 | rootCmd.AddCommand(productCommand("levant")) 27 | rootCmd.AddCommand(productCommand("consul-template")) 28 | rootCmd.AddCommand(productCommand("envconsul")) 29 | rootCmd.AddCommand(productCommand("nomad-pack")) 30 | 31 | return rootCmd.Execute() 32 | } 33 | 34 | func baseCommand(name string) *coral.Command { 35 | return &coral.Command{ 36 | Use: name, 37 | Run: func(cmd *coral.Command, args []string) { 38 | cmd.Help() 39 | }, 40 | SilenceErrors: true, 41 | } 42 | } 43 | 44 | func productCommand(name string, installer ...Installer) *coral.Command { 45 | command := baseCommand(name) 46 | command.Short = fmt.Sprintf("Install or download %s", strings.Title(name)) 47 | command.Long = fmt.Sprintf("Install or download %s", strings.Title(name)) 48 | command.AddCommand(GetCommand(name)) 49 | if installer != nil { 50 | for _, y := range installer { 51 | command.AddCommand(y()) 52 | } 53 | command.AddCommand(ManageServiceCommand("stop", name)) 54 | command.AddCommand(ManageServiceCommand("start", name)) 55 | command.AddCommand(ManageServiceCommand("restart", name)) 56 | command.AddCommand(ManageServiceCommand("reload", name)) 57 | command.AddCommand(UninstallCommand(name)) 58 | } 59 | return command 60 | } 61 | 62 | func expandPath(path string) string { 63 | res, _ := homedir.Expand(path) 64 | return res 65 | } 66 | 67 | func info(message string) { 68 | fmt.Println("[INFO] " + message) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/consul_install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/jsiebens/hashi-up/pkg/config" 9 | "github.com/jsiebens/hashi-up/pkg/operator" 10 | "github.com/jsiebens/hashi-up/scripts" 11 | "github.com/muesli/coral" 12 | "github.com/pkg/errors" 13 | "github.com/thanhpk/randstr" 14 | ) 15 | 16 | func InstallConsulCommand() *coral.Command { 17 | 18 | var skipConfig bool 19 | var skipEnable bool 20 | var skipStart bool 21 | var binary string 22 | var version string 23 | 24 | var configFile string 25 | var files []string 26 | 27 | var flags = config.ConsulConfig{} 28 | 29 | var command = &coral.Command{ 30 | Use: "install", 31 | Short: "Install Consul on a server via SSH", 32 | Long: "Install Consul on a server via SSH", 33 | SilenceUsage: true, 34 | } 35 | 36 | var target = Target{} 37 | target.prepareCommand(command) 38 | 39 | command.Flags().BoolVar(&skipConfig, "skip-config", false, "If set to true will install Consul service without touching existing config files") 40 | command.Flags().BoolVar(&skipEnable, "skip-enable", false, "If set to true will not enable or start Consul service") 41 | command.Flags().BoolVar(&skipStart, "skip-start", false, "If set to true will not start Consul service") 42 | command.Flags().StringVar(&binary, "package", "", "Upload and use this Consul package instead of downloading") 43 | command.Flags().StringVarP(&version, "version", "v", "", "Version of Consul to install") 44 | 45 | command.Flags().StringVarP(&configFile, "config-file", "c", "", "Custom Consul configuration file to upload, setting this will disable config file generation meaning the other flags are ignored") 46 | command.Flags().StringSliceVarP(&files, "file", "f", []string{}, "Additional files, e.g. certificates, to upload") 47 | 48 | command.Flags().BoolVar(&flags.Server, "server", false, "Consul: switches agent to server mode. (see Consul documentation for more info)") 49 | command.Flags().StringVar(&flags.Datacenter, "datacenter", "dc1", "Consul: specifies the data center of the local agent. (see Consul documentation for more info)") 50 | command.Flags().StringVar(&flags.BindAddr, "bind-addr", "", "Consul: sets the bind address for cluster communication. (see Consul documentation for more info)") 51 | command.Flags().StringVar(&flags.AdvertiseAddr, "advertise-addr", "", "Consul: sets the advertise address to use. (see Consul documentation for more info)") 52 | 53 | command.Flags().StringVar(&flags.ClientAddr, "client-addr", "", "Consul: sets the address to bind for client access. (see Consul documentation for more info)") 54 | command.Flags().StringVar(&flags.DnsAddr, "dns-addr", "", "Consul: sets the address for the DNS server. (see Consul documentation for more info)") 55 | command.Flags().StringVar(&flags.HttpAddr, "http-addr", "", "Consul: sets the address for the HTTP API server. (see Consul documentation for more info)") 56 | command.Flags().StringVar(&flags.HttpsAddr, "https-addr", "", "Consul: sets the address for the HTTPS API server. (see Consul documentation for more info)") 57 | command.Flags().StringVar(&flags.GrpcAddr, "grpc-addr", "", "Consul: sets the address for the gRPC API server. (see Consul documentation for more info)") 58 | 59 | command.Flags().Int64Var(&flags.BootstrapExpect, "bootstrap-expect", 1, "Consul: sets server to expect bootstrap mode. 0 are less disables bootstrap mode. (see Consul documentation for more info)") 60 | command.Flags().StringSliceVar(&flags.RetryJoin, "retry-join", []string{}, "Consul: address of an agent to join at start time with retries enabled. Can be specified multiple times. (see Consul documentation for more info)") 61 | command.Flags().StringVar(&flags.Encrypt, "encrypt", "", "Consul: provides the gossip encryption key. (see Consul documentation for more info)") 62 | command.Flags().StringVar(&flags.CaFile, "ca-file", "", "Consul: the certificate authority used to check the authenticity of client and server connections. (see Consul documentation for more info)") 63 | command.Flags().StringVar(&flags.CertFile, "cert-file", "", "Consul: the certificate to verify the agent's authenticity. (see Consul documentation for more info)") 64 | command.Flags().StringVar(&flags.KeyFile, "key-file", "", "Consul: the key used with the certificate to verify the agent's authenticity. (see Consul documentation for more info)") 65 | command.Flags().BoolVar(&flags.AutoEncrypt, "auto-encrypt", false, "Consul: this option enables auto_encrypt and allows servers to automatically distribute certificates from the Connect CA to the clients. (see Consul documentation for more info)") 66 | command.Flags().BoolVar(&flags.HttpsOnly, "https-only", true, "Consul: if true, HTTP port is disabled on both clients and servers and to only accept HTTPS connections when TLS enabled.") 67 | command.Flags().BoolVar(&flags.EnableConnect, "connect", false, "Consul: enables the Connect feature on the agent. (see Consul documentation for more info)") 68 | command.Flags().BoolVar(&flags.EnableACL, "acl", false, "Consul: enables Consul ACL system. (see Consul documentation for more info)") 69 | command.Flags().StringVar(&flags.AgentToken, "agent-token", "", "Consul: the token that the agent will use for internal agent operations.. (see Consul documentation for more info)") 70 | 71 | command.Flags().StringVar(&flags.ClientAddr, "client", "", "") 72 | command.Flags().StringVar(&flags.BindAddr, "bind", "", "") 73 | command.Flags().StringVar(&flags.AdvertiseAddr, "advertise", "", "") 74 | _ = command.Flags().MarkDeprecated("client", "use the new flag client-addr") 75 | _ = command.Flags().MarkDeprecated("bind", "use the new flag bind-addr") 76 | _ = command.Flags().MarkDeprecated("advertise", "use the new flag advertise-addr") 77 | 78 | command.RunE = func(command *coral.Command, args []string) error { 79 | if !target.Local && len(target.Addr) == 0 { 80 | return fmt.Errorf("required ssh-target-addr flag is missing") 81 | } 82 | 83 | ignoreConfigFlags := skipConfig || len(configFile) != 0 84 | 85 | var generatedConfig string 86 | 87 | if !ignoreConfigFlags { 88 | generatedConfig = flags.GenerateConfigFile() 89 | } 90 | 91 | if len(binary) == 0 && len(version) == 0 { 92 | latest, err := config.GetLatestVersion("consul") 93 | 94 | if err != nil { 95 | return errors.Wrapf(err, "unable to get latest version number, define a version manually with the --version flag") 96 | } 97 | 98 | version = latest 99 | } 100 | 101 | callback := func(op operator.CommandOperator) error { 102 | dir := "/tmp/hashi-up." + randstr.String(6) 103 | 104 | defer op.Execute("rm -rf " + dir) 105 | 106 | err := op.Execute("mkdir -p " + dir + "/config") 107 | if err != nil { 108 | return fmt.Errorf("error received during installation: %s", err) 109 | } 110 | 111 | if len(binary) != 0 { 112 | info("Uploading Consul package ...") 113 | err = op.UploadFile(binary, dir+"/consul.zip", "0640") 114 | if err != nil { 115 | return fmt.Errorf("error received during upload Consul package: %s", err) 116 | } 117 | } 118 | 119 | if !skipConfig { 120 | if !ignoreConfigFlags { 121 | info("Uploading generated Consul configuration ...") 122 | err = op.Upload(strings.NewReader(generatedConfig), dir+"/config/consul.hcl", "0640") 123 | if err != nil { 124 | return fmt.Errorf("error received during upload consul configuration: %s", err) 125 | } 126 | 127 | files = []string{} 128 | 129 | if flags.EnableTLS() { 130 | files = []string{flags.CaFile, flags.CertFile, flags.KeyFile} 131 | } 132 | } else { 133 | info(fmt.Sprintf("Uploading %s as consul.hcl...", configFile)) 134 | err = op.UploadFile(expandPath(configFile), dir+"/config/consul.hcl", "0640") 135 | if err != nil { 136 | return fmt.Errorf("error received during upload consul configuration: %s", err) 137 | } 138 | } 139 | 140 | for _, s := range files { 141 | if len(s) != 0 { 142 | info(fmt.Sprintf("Uploading %s...", s)) 143 | _, filename := filepath.Split(expandPath(s)) 144 | err = op.UploadFile(expandPath(s), dir+"/config/"+filename, "0640") 145 | if err != nil { 146 | return fmt.Errorf("error received during upload consul ca file: %s", err) 147 | } 148 | } 149 | } 150 | } 151 | 152 | data := map[string]interface{}{ 153 | "TmpDir": dir, 154 | "SkipEnable": skipEnable, 155 | "SkipStart": skipStart, 156 | "Version": version, 157 | "ArmSuffix": config.GetArmSuffix("consul", version), 158 | } 159 | 160 | installScript, err := scripts.RenderScript("install_consul.sh", data) 161 | 162 | if err != nil { 163 | return err 164 | } 165 | 166 | err = op.Upload(installScript, dir+"/install.sh", "0755") 167 | if err != nil { 168 | return fmt.Errorf("error received during upload install script: %s", err) 169 | } 170 | 171 | info("Installing Consul ...") 172 | sudoPass, err := target.sudoPass() 173 | if err != nil { 174 | return fmt.Errorf("error received during installation: %s", err) 175 | } 176 | err = op.Execute(fmt.Sprintf("cat %s/install.sh | SUDO_PASS=\"%s\" sh -\n", dir, sudoPass)) 177 | if err != nil { 178 | return fmt.Errorf("error received during installation: %s", err) 179 | } 180 | 181 | info("Done.") 182 | 183 | return nil 184 | } 185 | 186 | return target.execute(callback) 187 | } 188 | 189 | return command 190 | } 191 | -------------------------------------------------------------------------------- /cmd/get.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Masterminds/semver" 6 | "io" 7 | "net/http" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | 14 | "github.com/cheggaaa/pb/v3" 15 | "github.com/jsiebens/hashi-up/pkg/archive" 16 | "github.com/jsiebens/hashi-up/pkg/config" 17 | "github.com/muesli/coral" 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | func GetCommand(product string) *coral.Command { 22 | 23 | var version string 24 | var arch string 25 | var destination string 26 | var extract bool 27 | 28 | var command = &coral.Command{ 29 | Use: "get", 30 | Short: fmt.Sprintf("Download %s on your local machine", strings.Title(product)), 31 | Long: fmt.Sprintf("Download %s on your local machine", strings.Title(product)), 32 | SilenceUsage: true, 33 | } 34 | 35 | title := strings.Title(product) 36 | 37 | command.Flags().StringVarP(&version, "version", "v", "", fmt.Sprintf("Version of %s to install", title)) 38 | command.Flags().StringVar(&arch, "arch", runtime.GOARCH, "Target architecture") 39 | command.Flags().BoolVar(&extract, "extract", true, "Extract the binary from the downloaded archive") 40 | command.Flags().StringVarP(&destination, "dest", "d", expandPath("~/bin"), "Target directory for the downloaded archive or binary") 41 | 42 | command.RunE = func(command *coral.Command, args []string) error { 43 | 44 | if len(version) == 0 { 45 | latest, err := config.GetLatestVersion(product) 46 | 47 | if err != nil { 48 | return errors.Wrapf(err, "unable to get latest version number, define a version manually with the --version flag") 49 | } 50 | 51 | version = latest 52 | } 53 | 54 | semVersion, err := semver.NewVersion(version) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | file, err := downloadFile(config.GetDownloadURL(product, arch, semVersion)) 60 | 61 | if err != nil { 62 | return errors.Wrapf(err, "unable to download %s distribution", title) 63 | } 64 | 65 | if extract { 66 | if err := archive.Unzip(file, destination); err != nil { 67 | return errors.Wrapf(err, "unable to install %s distribution", title) 68 | } 69 | } else { 70 | if err := os.Rename(file, filepath.Join(destination, filepath.Base(file))); err != nil { 71 | return errors.Wrapf(err, "unable to install %s distribution", title) 72 | } 73 | } 74 | 75 | return nil 76 | } 77 | 78 | return command 79 | } 80 | 81 | func downloadFile(downloadURL string) (string, error) { 82 | fmt.Printf("Downloading file %s \n", downloadURL) 83 | res, err := http.DefaultClient.Get(downloadURL) 84 | if err != nil { 85 | return "", err 86 | } 87 | 88 | if res.Body != nil { 89 | defer res.Body.Close() 90 | } 91 | 92 | if res.StatusCode != http.StatusOK { 93 | return "", fmt.Errorf("incorrect status for downloading %s: %d", downloadURL, res.StatusCode) 94 | } 95 | 96 | _, fileName := path.Split(downloadURL) 97 | tmp := os.TempDir() 98 | outFilePath := path.Join(tmp, fileName) 99 | wrappedReader := withProgressBar(res.Body, int(res.ContentLength)) 100 | out, err := os.Create(outFilePath) 101 | if err != nil { 102 | return "", err 103 | } 104 | 105 | defer out.Close() 106 | defer wrappedReader.Close() 107 | 108 | if _, err := io.Copy(out, wrappedReader); err != nil { 109 | return "", err 110 | } 111 | 112 | return outFilePath, nil 113 | } 114 | 115 | func withProgressBar(r io.ReadCloser, length int) io.ReadCloser { 116 | bar := pb.Simple.New(length).Start() 117 | return bar.NewProxyReader(r) 118 | } 119 | -------------------------------------------------------------------------------- /cmd/nomad_install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/jsiebens/hashi-up/pkg/config" 9 | "github.com/jsiebens/hashi-up/pkg/operator" 10 | "github.com/jsiebens/hashi-up/scripts" 11 | "github.com/muesli/coral" 12 | "github.com/pkg/errors" 13 | "github.com/thanhpk/randstr" 14 | ) 15 | 16 | func InstallNomadCommand() *coral.Command { 17 | 18 | var skipConfig bool 19 | var skipEnable bool 20 | var skipStart bool 21 | var binary string 22 | var version string 23 | 24 | var configFile string 25 | var files []string 26 | 27 | var flags = config.NomadConfig{} 28 | 29 | var command = &coral.Command{ 30 | Use: "install", 31 | Short: "Install Nomad on a server via SSH", 32 | Long: "Install Nomad on a server via SSH", 33 | SilenceUsage: true, 34 | } 35 | 36 | var target = Target{} 37 | target.prepareCommand(command) 38 | 39 | command.Flags().BoolVar(&skipConfig, "skip-config", false, "If set to true will install Nomad service without touching existing config files") 40 | command.Flags().BoolVar(&skipEnable, "skip-enable", false, "If set to true will not enable or start Nomad service") 41 | command.Flags().BoolVar(&skipStart, "skip-start", false, "If set to true will not start Nomad service") 42 | command.Flags().StringVar(&binary, "package", "", "Upload and use this Nomad package instead of downloading") 43 | command.Flags().StringVarP(&version, "version", "v", "", "Version of Nomad to install") 44 | 45 | command.Flags().StringVarP(&configFile, "config-file", "c", "", "Custom Nomad configuration file to upload, setting this will disable config file generation meaning the other flags are ignored") 46 | command.Flags().StringSliceVarP(&files, "file", "f", []string{}, "Additional files, e.g. certificates, to upload") 47 | 48 | command.Flags().BoolVar(&flags.Server, "server", false, "Nomad: enables the server mode of the agent. (see Nomad documentation for more info)") 49 | command.Flags().BoolVar(&flags.Client, "client", false, "Nomad: enables the client mode of the agent. (see Nomad documentation for more info)") 50 | command.Flags().StringVar(&flags.Datacenter, "datacenter", "dc1", "Nomad: specifies the data center of the local agent. (see Nomad documentation for more info)") 51 | command.Flags().StringVar(&flags.NodeClass, "node-class", "", "Nomad: specifies an arbitrary string used to logically group client nodes by user-defined class. (see Nomad documentation for more info)") 52 | command.Flags().StringVar(&flags.BindAddr, "address", "", "Nomad: the address the agent will bind to for all of its various network services. (see Nomad documentation for more info)") 53 | command.Flags().StringVar(&flags.AdvertiseAddr, "advertise", "", "Nomad: the address the agent will advertise to for all of its various network services. (see Nomad documentation for more info)") 54 | command.Flags().Int64Var(&flags.BootstrapExpect, "bootstrap-expect", 1, "Nomad: sets server to expect bootstrap mode. 0 are less disables bootstrap mode. (see Nomad documentation for more info)") 55 | command.Flags().StringSliceVar(&flags.RetryJoin, "retry-join", []string{}, "Nomad: address of an agent to join at start time with retries enabled. Can be specified multiple times. (see Nomad documentation for more info)") 56 | command.Flags().StringVar(&flags.Encrypt, "encrypt", "", "Nomad: Provides the gossip encryption key. (see Nomad documentation for more info)") 57 | command.Flags().StringVar(&flags.CaFile, "ca-file", "", "Nomad: the certificate authority used to check the authenticity of client and server connections. (see Nomad documentation for more info)") 58 | command.Flags().StringVar(&flags.CertFile, "cert-file", "", "Nomad: the certificate to verify the agent's authenticity. (see Nomad documentation for more info)") 59 | command.Flags().StringVar(&flags.KeyFile, "key-file", "", "Nomad: the key used with the certificate to verify the agent's authenticity. (see Nomad documentation for more info)") 60 | command.Flags().BoolVar(&flags.EnableACL, "acl", false, "Nomad: enables Nomad ACL system. (see Nomad documentation for more info)") 61 | 62 | command.RunE = func(command *coral.Command, args []string) error { 63 | if !target.Local && len(target.Addr) == 0 { 64 | return fmt.Errorf("required ssh-target-addr flag is missing") 65 | } 66 | 67 | ignoreConfigFlags := skipConfig || len(configFile) != 0 68 | 69 | var generatedConfig string 70 | 71 | if !ignoreConfigFlags { 72 | generatedConfig = flags.GenerateConfigFile() 73 | } 74 | 75 | if len(binary) == 0 && len(version) == 0 { 76 | latest, err := config.GetLatestVersion("nomad") 77 | 78 | if err != nil { 79 | return errors.Wrapf(err, "unable to get latest version number, define a version manually with the --version flag") 80 | } 81 | 82 | version = latest 83 | } 84 | 85 | callback := func(op operator.CommandOperator) error { 86 | dir := "/tmp/hashi-up." + randstr.String(6) 87 | 88 | defer op.Execute("rm -rf " + dir) 89 | 90 | err := op.Execute("mkdir -p " + dir + "/config") 91 | if err != nil { 92 | return fmt.Errorf("error received during installation: %s", err) 93 | } 94 | 95 | if len(binary) != 0 { 96 | info("Uploading Nomad package ...") 97 | err = op.UploadFile(binary, dir+"/nomad.zip", "0640") 98 | if err != nil { 99 | return fmt.Errorf("error received during upload nomad package: %s", err) 100 | } 101 | } 102 | 103 | if !skipConfig { 104 | if !ignoreConfigFlags { 105 | info("Uploading generated Nomad configuration ...") 106 | err = op.Upload(strings.NewReader(generatedConfig), dir+"/config/nomad.hcl", "0640") 107 | if err != nil { 108 | return fmt.Errorf("error received during upload nomad configuration: %s", err) 109 | } 110 | 111 | files = []string{} 112 | 113 | if flags.EnableTLS() { 114 | files = []string{flags.CaFile, flags.KeyFile, flags.CertFile} 115 | } 116 | } else { 117 | info(fmt.Sprintf("Uploading %s as nomad.hcl...", configFile)) 118 | err = op.UploadFile(expandPath(configFile), dir+"/config/nomad.hcl", "0640") 119 | if err != nil { 120 | return fmt.Errorf("error received during upload nomad configuration: %s", err) 121 | } 122 | } 123 | 124 | for _, s := range files { 125 | if len(s) != 0 { 126 | info(fmt.Sprintf("Uploading %s...", s)) 127 | _, filename := filepath.Split(expandPath(s)) 128 | err = op.UploadFile(expandPath(s), dir+"/config/"+filename, "0640") 129 | if err != nil { 130 | return fmt.Errorf("error received during upload file: %s", err) 131 | } 132 | } 133 | } 134 | } 135 | 136 | data := map[string]interface{}{ 137 | "TmpDir": dir, 138 | "SkipEnable": skipEnable, 139 | "SkipStart": skipStart, 140 | "Version": version, 141 | } 142 | 143 | installScript, err := scripts.RenderScript("install_nomad.sh", data) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | err = op.Upload(installScript, dir+"/install.sh", "0755") 149 | if err != nil { 150 | return fmt.Errorf("error received during upload install script: %s", err) 151 | } 152 | 153 | info("Installing Nomad ...") 154 | sudoPass, err := target.sudoPass() 155 | if err != nil { 156 | return fmt.Errorf("error received during installation: %s", err) 157 | } 158 | err = op.Execute(fmt.Sprintf("cat %s/install.sh | SUDO_PASS=\"%s\" sh -\n", dir, sudoPass)) 159 | if err != nil { 160 | return fmt.Errorf("error received during installation: %s", err) 161 | } 162 | 163 | info("Done.") 164 | 165 | return nil 166 | } 167 | 168 | return target.execute(callback) 169 | } 170 | 171 | return command 172 | } 173 | -------------------------------------------------------------------------------- /cmd/service.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/jsiebens/hashi-up/pkg/operator" 8 | "github.com/jsiebens/hashi-up/scripts" 9 | "github.com/muesli/coral" 10 | "github.com/thanhpk/randstr" 11 | ) 12 | 13 | func ManageServiceCommand(action, product string) *coral.Command { 14 | 15 | var command = &coral.Command{ 16 | Use: action, 17 | Short: fmt.Sprintf("%s %s systemd service on a server via SSH", strings.Title(action), strings.Title(product)), 18 | Long: fmt.Sprintf("%s %s systemd service on a server via SSH", strings.Title(action), strings.Title(product)), 19 | SilenceUsage: true, 20 | } 21 | 22 | var target = Target{} 23 | target.prepareCommand(command) 24 | 25 | command.RunE = func(command *coral.Command, args []string) error { 26 | if !target.Local && len(target.Addr) == 0 { 27 | return fmt.Errorf("required ssh-target-addr flag is missing") 28 | } 29 | 30 | callback := func(op operator.CommandOperator) error { 31 | dir := "/tmp/hashi-up." + randstr.String(6) 32 | 33 | defer op.Execute("rm -rf " + dir) 34 | 35 | err := op.Execute("mkdir -p " + dir) 36 | if err != nil { 37 | return fmt.Errorf("error received during preparation: %s", err) 38 | } 39 | 40 | installScript, err := scripts.Open("service.sh") 41 | 42 | if err != nil { 43 | return err 44 | } 45 | 46 | defer installScript.Close() 47 | 48 | err = op.Upload(installScript, dir+"/run.sh", "0755") 49 | if err != nil { 50 | return fmt.Errorf("error received during upload install script: %s", err) 51 | } 52 | 53 | info(fmt.Sprintf("%sing %s ...", strings.Title(action), strings.Title(product))) 54 | sudoPass, err := target.sudoPass() 55 | if err != nil { 56 | return fmt.Errorf("error received during execution: %s", err) 57 | } 58 | err = op.Execute(fmt.Sprintf("cat %s/run.sh | ACTION=%s SERVICE=%s SUDO_PASS=\"%s\" sh -\n", dir, action, product, sudoPass)) 59 | if err != nil { 60 | return fmt.Errorf("error received during execution: %s", err) 61 | } 62 | 63 | info("Done.") 64 | 65 | return nil 66 | } 67 | 68 | return target.execute(callback) 69 | } 70 | 71 | return command 72 | } 73 | -------------------------------------------------------------------------------- /cmd/target.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strings" 7 | 8 | "github.com/jsiebens/hashi-up/pkg/operator" 9 | "github.com/mitchellh/go-homedir" 10 | "github.com/muesli/coral" 11 | ) 12 | 13 | const SshTargetPassword = "SSH_TARGET_PASSWORD" 14 | const SshTargetSudoPass = "SSH_TARGET_SUDO_PASS" 15 | 16 | type Target struct { 17 | Addr string 18 | User string 19 | Key string 20 | Password string 21 | SudoPass string 22 | Local bool 23 | } 24 | 25 | func (t *Target) prepareCommand(cmd *coral.Command) { 26 | cmd.Flags().StringVarP(&t.Addr, "ssh-target-addr", "r", "", "Remote SSH target address (e.g. 127.0.0.1:22") 27 | cmd.Flags().StringVarP(&t.User, "ssh-target-user", "u", "root", "Username for SSH login") 28 | cmd.Flags().StringVarP(&t.Key, "ssh-target-key", "k", "", "The ssh key to use for SSH login") 29 | cmd.Flags().StringVarP(&t.Password, "ssh-target-password", "p", "", "The ssh password to use for SSH login") 30 | cmd.Flags().StringVarP(&t.SudoPass, "ssh-target-sudo-pass", "s", "", "The ssh password to use for SSH login") 31 | cmd.Flags().BoolVar(&t.Local, "local", false, "Running the installation locally, without ssh") 32 | } 33 | 34 | func (t *Target) execute(callback operator.Callback) error { 35 | if t.Local { 36 | return operator.ExecuteLocal(callback) 37 | } else { 38 | pwd, err := pathOrContents(getenv(SshTargetPassword, t.Password)) 39 | if err != nil { 40 | return err 41 | } 42 | return operator.ExecuteRemote(t.Addr, t.User, t.Key, pwd, callback) 43 | } 44 | } 45 | 46 | func (t *Target) sudoPass() (string, error) { 47 | sudoPass := getenv(SshTargetSudoPass, t.SudoPass) 48 | if len(sudoPass) != 0 { 49 | pwd, err := pathOrContents(sudoPass) 50 | if err != nil { 51 | return "", err 52 | } else { 53 | return pwd, nil 54 | } 55 | } else { 56 | pwd, err := pathOrContents(getenv(SshTargetPassword, t.Password)) 57 | if err != nil { 58 | return "", err 59 | } else { 60 | return pwd, nil 61 | } 62 | } 63 | } 64 | 65 | func pathOrContents(poc string) (string, error) { 66 | if len(poc) == 0 { 67 | return poc, nil 68 | } 69 | 70 | path := poc 71 | if path[0] == '~' { 72 | var err error 73 | path, err = homedir.Expand(path) 74 | if err != nil { 75 | return path, err 76 | } 77 | } 78 | 79 | if _, err := os.Stat(path); err == nil { 80 | contents, err := ioutil.ReadFile(path) 81 | if err != nil { 82 | return string(contents), err 83 | } 84 | return strings.TrimSpace(string(contents)), nil 85 | } 86 | 87 | return poc, nil 88 | } 89 | 90 | func getenv(key, override string) string { 91 | if len(override) != 0 { 92 | return override 93 | } 94 | value := os.Getenv(key) 95 | if len(value) != 0 { 96 | return value 97 | } 98 | return "" 99 | } 100 | -------------------------------------------------------------------------------- /cmd/tls.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "fmt" 10 | "math/big" 11 | "net" 12 | "os" 13 | "time" 14 | 15 | "github.com/muesli/coral" 16 | ) 17 | 18 | func TlsCommands() *coral.Command { 19 | tlsCmd := baseCommand("tls") 20 | tlsCmd.Short = "Builtin helpers for creating certificates" 21 | tlsCmd.Long = "Builtin helpers for creating certificates" 22 | tlsCmd.AddCommand(certCommands()) 23 | return tlsCmd 24 | } 25 | 26 | func certCommands() *coral.Command { 27 | certCmd := baseCommand("cert") 28 | certCmd.AddCommand(createCertificateCommand()) 29 | return certCmd 30 | } 31 | 32 | func createCertificateCommand() *coral.Command { 33 | 34 | var hosts []string 35 | 36 | command := &coral.Command{ 37 | Use: "create", 38 | SilenceUsage: true, 39 | } 40 | 41 | command.Flags().StringSliceVar(&hosts, "host", []string{}, "Hostnames and IPs to generate a certificate for") 42 | 43 | command.RunE = func(command *coral.Command, args []string) error { 44 | 45 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 46 | 47 | if err != nil { 48 | return err 49 | } 50 | 51 | keyUsage := x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment 52 | 53 | notBefore := time.Now() 54 | notAfter := notBefore.Add(365 * 24 * time.Hour) 55 | 56 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 57 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | template := x509.Certificate{ 63 | SerialNumber: serialNumber, 64 | NotBefore: notBefore, 65 | NotAfter: notAfter, 66 | 67 | Subject: pkix.Name{ 68 | CommonName: "hashi-up!", 69 | }, 70 | 71 | KeyUsage: keyUsage, 72 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 73 | BasicConstraintsValid: true, 74 | } 75 | 76 | for _, h := range hosts { 77 | if ip := net.ParseIP(h); ip != nil { 78 | template.IPAddresses = append(template.IPAddresses, ip) 79 | } else { 80 | template.DNSNames = append(template.DNSNames, h) 81 | } 82 | } 83 | 84 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, privateKey.Public(), privateKey) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | certOut, err := os.Create("server.pem") 90 | if err != nil { 91 | return err 92 | } 93 | if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { 94 | return err 95 | } 96 | if err := certOut.Close(); err != nil { 97 | return err 98 | } 99 | fmt.Println("wrote server.pem") 100 | 101 | keyOut, err := os.OpenFile("server-key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) 102 | if err != nil { 103 | return err 104 | } 105 | privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) 106 | if err != nil { 107 | return err 108 | } 109 | if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil { 110 | return err 111 | } 112 | if err := keyOut.Close(); err != nil { 113 | return err 114 | } 115 | 116 | fmt.Println("wrote server-key.pem") 117 | 118 | return nil 119 | } 120 | 121 | return command 122 | } 123 | -------------------------------------------------------------------------------- /cmd/uninstall.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/jsiebens/hashi-up/pkg/operator" 8 | "github.com/jsiebens/hashi-up/scripts" 9 | "github.com/muesli/coral" 10 | "github.com/thanhpk/randstr" 11 | ) 12 | 13 | func UninstallCommand(product string) *coral.Command { 14 | 15 | var command = &coral.Command{ 16 | Use: "uninstall", 17 | Short: fmt.Sprintf("Uninstall %s on a server via SSH", strings.Title(product)), 18 | Long: fmt.Sprintf("Uninstall %s on a server via SSH", strings.Title(product)), 19 | SilenceUsage: true, 20 | } 21 | 22 | var target = Target{} 23 | target.prepareCommand(command) 24 | 25 | command.RunE = func(command *coral.Command, args []string) error { 26 | if !target.Local && len(target.Addr) == 0 { 27 | return fmt.Errorf("required ssh-target-addr flag is missing") 28 | } 29 | 30 | callback := func(op operator.CommandOperator) error { 31 | dir := "/tmp/hashi-up." + randstr.String(6) 32 | 33 | defer op.Execute("rm -rf " + dir) 34 | 35 | err := op.Execute("mkdir -p " + dir + "/config") 36 | if err != nil { 37 | return fmt.Errorf("error received during installation: %s", err) 38 | } 39 | 40 | installScript, err := scripts.Open("uninstall.sh") 41 | 42 | if err != nil { 43 | return err 44 | } 45 | 46 | defer installScript.Close() 47 | 48 | err = op.Upload(installScript, dir+"/run.sh", "0755") 49 | if err != nil { 50 | return fmt.Errorf("error received during upload install script: %s", err) 51 | } 52 | 53 | info(fmt.Sprintf("Uninstalling %s ...", strings.Title(product))) 54 | sudoPass, err := target.sudoPass() 55 | if err != nil { 56 | return fmt.Errorf("error received during installation: %s", err) 57 | } 58 | err = op.Execute(fmt.Sprintf("cat %s/run.sh | SERVICE=%s SUDO_PASS=\"%s\" sh -\n", dir, product, sudoPass)) 59 | if err != nil { 60 | return fmt.Errorf("error received during uninstallation: %s", err) 61 | } 62 | 63 | info("Done.") 64 | 65 | return nil 66 | } 67 | 68 | return target.execute(callback) 69 | } 70 | 71 | return command 72 | } 73 | -------------------------------------------------------------------------------- /cmd/vault_install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/jsiebens/hashi-up/pkg/config" 9 | "github.com/jsiebens/hashi-up/pkg/operator" 10 | "github.com/jsiebens/hashi-up/scripts" 11 | "github.com/muesli/coral" 12 | "github.com/pkg/errors" 13 | "github.com/thanhpk/randstr" 14 | ) 15 | 16 | func InstallVaultCommand() *coral.Command { 17 | 18 | var skipConfig bool 19 | var skipEnable bool 20 | var skipStart bool 21 | var binary string 22 | var version string 23 | 24 | var configFile string 25 | var files []string 26 | 27 | var flags = config.VaultConfig{} 28 | 29 | var command = &coral.Command{ 30 | Use: "install", 31 | Short: "Install Vault on a server via SSH", 32 | Long: "Install Vault on a server via SSH", 33 | SilenceUsage: true, 34 | } 35 | 36 | var target = Target{} 37 | target.prepareCommand(command) 38 | 39 | command.Flags().BoolVar(&skipConfig, "skip-config", false, "If set to true will install Vault service without touching existing config files") 40 | command.Flags().BoolVar(&skipEnable, "skip-enable", false, "If set to true will not enable or start Vault service") 41 | command.Flags().BoolVar(&skipStart, "skip-start", false, "If set to true will not start Vault service") 42 | command.Flags().StringVar(&binary, "package", "", "Upload and use this Vault package instead of downloading") 43 | command.Flags().StringVarP(&version, "version", "v", "", "Version of Vault to install") 44 | 45 | command.Flags().StringVarP(&configFile, "config-file", "c", "", "Custom Vault configuration file to upload, setting this will disable config file generation meaning the other flags are ignored") 46 | command.Flags().StringSliceVarP(&files, "file", "f", []string{}, "Additional files, e.g. certificates, to upload") 47 | 48 | command.Flags().StringVar(&flags.CertFile, "cert-file", "", "Vault: the certificate for TLS. (see Vault documentation for more info)") 49 | command.Flags().StringVar(&flags.KeyFile, "key-file", "", "Vault: the private key for the certificate. (see Vault documentation for more info)") 50 | command.Flags().StringSliceVar(&flags.Address, "address", []string{"0.0.0.0:8200"}, "Vault: the address to bind to for listening. (see Vault documentation for more info)") 51 | command.Flags().StringVar(&flags.ApiAddr, "api-addr", "", "Vault: the address (full URL) to advertise to other Vault servers in the cluster for client redirection. (see Vault documentation for more info)") 52 | command.Flags().StringVar(&flags.ClusterAddr, "cluster-addr", "", "Vault: the address to advertise to other Vault servers in the cluster for request forwarding. (see Vault documentation for more info)") 53 | command.Flags().StringVar(&flags.Storage, "storage", "file", "Vault: the type of storage backend. Currently only \"file\" of \"consul\" is supported. (see Vault documentation for more info)") 54 | command.Flags().StringVar(&flags.ConsulAddr, "consul-addr", "127.0.0.1:8500", "Vault: the address of the Consul agent to communicate with. (see Vault documentation for more info)") 55 | command.Flags().StringVar(&flags.ConsulPath, "consul-path", "vault/", "Vault: the path in Consul's key-value store where Vault data will be stored. (see Vault documentation for more info)") 56 | command.Flags().StringVar(&flags.ConsulToken, "consul-token", "", "Vault: the Consul ACL token with permission to read and write from the path in Consul's key-value store. (see Vault documentation for more info)") 57 | command.Flags().StringVar(&flags.ConsulCaFile, "consul-tls-ca-file", "", "Vault: the path to the CA certificate used for Consul communication. (see Vault documentation for more info)") 58 | command.Flags().StringVar(&flags.ConsulCertFile, "consul-tls-cert-file", "", "Vault: the path to the certificate for Consul communication. (see Vault documentation for more info)") 59 | command.Flags().StringVar(&flags.ConsulKeyFile, "consul-tls-key-file", "", "Vault: the path to the private key for Consul communication. (see Vault documentation for more info)") 60 | 61 | command.RunE = func(command *coral.Command, args []string) error { 62 | if !target.Local && len(target.Addr) == 0 { 63 | return fmt.Errorf("required ssh-target-addr flag is missing") 64 | } 65 | 66 | ignoreConfigFlags := skipConfig || len(configFile) != 0 67 | 68 | var generatedConfig string 69 | 70 | if !ignoreConfigFlags { 71 | generatedConfig = flags.GenerateConfigFile() 72 | } 73 | 74 | if len(binary) == 0 && len(version) == 0 { 75 | latest, err := config.GetLatestVersion("vault") 76 | 77 | if err != nil { 78 | return errors.Wrapf(err, "unable to get latest version number, define a version manually with the --version flag") 79 | } 80 | 81 | version = latest 82 | } 83 | 84 | callback := func(op operator.CommandOperator) error { 85 | dir := "/tmp/hashi-up." + randstr.String(6) 86 | 87 | defer op.Execute("rm -rf " + dir) 88 | 89 | err := op.Execute("mkdir -p " + dir + "/config") 90 | if err != nil { 91 | return fmt.Errorf("error received during installation: %s", err) 92 | } 93 | 94 | if len(binary) != 0 { 95 | info("Uploading Vault package ...") 96 | err = op.UploadFile(binary, dir+"/vault.zip", "0644") 97 | if err != nil { 98 | return fmt.Errorf("error received during upload Vault package: %s", err) 99 | } 100 | } 101 | 102 | if !skipConfig { 103 | if !ignoreConfigFlags { 104 | info("Uploading generated Vault configuration ...") 105 | err = op.Upload(strings.NewReader(generatedConfig), dir+"/config/vault.hcl", "0640") 106 | if err != nil { 107 | return fmt.Errorf("error received during upload consul configuration: %s", err) 108 | } 109 | 110 | files = []string{} 111 | 112 | if flags.EnableTLS() { 113 | files = append(files, flags.KeyFile, flags.CertFile) 114 | } 115 | 116 | if flags.EnableConsulTLS() { 117 | files = append(files, flags.ConsulCaFile, flags.ConsulCertFile, flags.ConsulKeyFile) 118 | } 119 | } else { 120 | info(fmt.Sprintf("Uploading %s as vault.hcl...", configFile)) 121 | err = op.UploadFile(expandPath(configFile), dir+"/config/vault.hcl", "0640") 122 | if err != nil { 123 | return fmt.Errorf("error received during upload nomad configuration: %s", err) 124 | } 125 | } 126 | 127 | for _, s := range files { 128 | if len(s) != 0 { 129 | info(fmt.Sprintf("Uploading %s...", s)) 130 | _, filename := filepath.Split(expandPath(s)) 131 | err = op.UploadFile(expandPath(s), dir+"/config/"+filename, "0640") 132 | if err != nil { 133 | return fmt.Errorf("error received during upload file: %s", err) 134 | } 135 | } 136 | } 137 | } 138 | 139 | data := map[string]interface{}{ 140 | "TmpDir": dir, 141 | "SkipEnable": skipEnable, 142 | "SkipStart": skipStart, 143 | "Version": version, 144 | } 145 | 146 | installScript, err := scripts.RenderScript("install_vault.sh", data) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | err = op.Upload(installScript, dir+"/install.sh", "0755") 152 | if err != nil { 153 | return fmt.Errorf("error received during upload install script: %s", err) 154 | } 155 | 156 | info("Installing Vault ...") 157 | sudoPass, err := target.sudoPass() 158 | if err != nil { 159 | return fmt.Errorf("error received during installation: %s", err) 160 | } 161 | err = op.Execute(fmt.Sprintf("cat %s/install.sh | SUDO_PASS=\"%s\" sh -\n", dir, sudoPass)) 162 | if err != nil { 163 | return fmt.Errorf("error received during installation: %s", err) 164 | } 165 | 166 | info("Done.") 167 | 168 | return nil 169 | } 170 | 171 | return target.execute(callback) 172 | } 173 | 174 | return command 175 | } 176 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/muesli/coral" 7 | ) 8 | 9 | var ( 10 | Version string 11 | GitCommit string 12 | ) 13 | 14 | func VersionCommand() *coral.Command { 15 | var command = &coral.Command{ 16 | Use: "version", 17 | Short: "Prints the hashi-up version", 18 | Long: "Prints the hashi-up version", 19 | SilenceUsage: true, 20 | } 21 | 22 | command.Run = func(cmd *coral.Command, args []string) { 23 | if len(Version) == 0 { 24 | fmt.Println("Version: dev") 25 | } else { 26 | fmt.Println("Version:", Version) 27 | } 28 | fmt.Println("Git Commit:", GitCommit) 29 | } 30 | return command 31 | } 32 | -------------------------------------------------------------------------------- /docs/boundary.md: -------------------------------------------------------------------------------- 1 | # Installing Boundary with hashi-up 2 | 3 | There are two ways to install Boundary with `hashi-up`. 4 | 5 | By default, `hashi-up` will generate a Boundary configuration file with values based on the CLI flags. 6 | Boundary has a lot of configuration options, and `hashi-up` tries to provide the most convenient parts as CLI options. 7 | This allows you to install Boundary without creating a configuration file. 8 | 9 | Especially regarding the KMS block, Boundary has support for many different implementations like AWS KMS, GCP Cloud KMS,... 10 | Using the command-line flags, hashi-up only supports the AEAD implementation as the keys can be provided as CLI arguments. 11 | 12 | For more advanced use cases, use the `--config-file` flags to upload an existing configuration files, together with additional resources like certificates. 13 | 14 | ## Installing Boundary 15 | 16 | ### Set up a single Boundary server 17 | 18 | Provision a new VM running a compatible operating system such as Ubuntu, Debian, Raspbian, or something else. 19 | Make sure that you opt-in to copy your registered SSH keys over to the new VM or host automatically. 20 | 21 | > Note: You can copy ssh keys to a remote VM with `ssh-copy-id user@IP`. 22 | 23 | Imagine the IP was `192.168.0.100` and the username was `ubuntu`, then you would run this: 24 | 25 | > The following steps assume that a Postgres server and database is available on the target host 26 | 27 | ```sh 28 | export IP=192.168.0.100 29 | export ROOT_KEY=JuZetUPcWEbsO4cTP7/E93O8Zrl6CQnYYb25CLFVAJM= 30 | export WORKER_AUTH_KEY=uDuFypRqVPq7DM0Ujgrl5PoRRgqqy4eFdW7ORNIHHhU= 31 | export RECOVERY_KEY=eixF4zdao9C6Soe+5jzBTtCnLUGMsP72NW5x30QP6So= 32 | 33 | # initialize the database 34 | hashi-up boundary init-database \ 35 | --ssh-target-addr $IP \ 36 | --db-url "postgresql://boundary:boundary123@localhost:5432/boundary?sslmode=disable" \ 37 | --root-key $ROOT_KEY 38 | 39 | # install Boundary running a controller and a worker 40 | hashi-up boundary install \ 41 | --ssh-target-addr $IP \ 42 | --controller-name boundary-controller \ 43 | --worker-name boundary-worker \ 44 | --db-url "postgresql://boundary:boundary123@localhost:5432/boundary?sslmode=disable" \ 45 | --cluster-addr 127.0.0.1 \ 46 | --public-addr $IP \ 47 | --root-key $ROOT_KEY \ 48 | --worker-auth-key $WORKER_AUTH_KEY \ 49 | --recovery-key $RECOVERY_KEY \ 50 | --controller 127.0.0.1 51 | ``` 52 | 53 | The first command will initialize the database and prints out the detailed information to get you started with a default user and default targets. 54 | 55 | When the commands finish, try to access Boundary using the UI at http://192.168.100:9200 or with the cli 56 | 57 | ### Create a multi-server (HA) setup 58 | 59 | Boundary supports a multi-server mode for [high availability](https://www.boundaryproject.io/docs/installing/high-availability). 60 | In this example we will create a single controller and two separate worker nodes. 61 | 62 | Prepare, for example, 3 nodes and let's say they have the following ip addresses: 63 | 64 | - 192.168.0.100 65 | - 192.168.0.101 66 | - 192.168.0.102 67 | 68 | > The following steps assume that a Postgres server and database is available on the target host where we will install the Boundary Controller 69 | 70 | First initialize the Boundary database: 71 | 72 | ``` sh 73 | export CONTROLLER_IP=192.168.0.100 74 | export ROOT_KEY=JuZetUPcWEbsO4cTP7/E93O8Zrl6CQnYYb25CLFVAJM= 75 | 76 | hashi-up boundary init-database \ 77 | --ssh-target-addr $CONTROLLER_IP \ 78 | --db-url "postgresql://boundary:boundary123@localhost:5432/boundary?sslmode=disable" \ 79 | --root-key $ROOT_KEY 80 | ``` 81 | 82 | Next, install a Boundary Controller on the first node: 83 | 84 | ``` sh 85 | export CONTROLLER_IP=192.168.0.100 86 | export ROOT_KEY=JuZetUPcWEbsO4cTP7/E93O8Zrl6CQnYYb25CLFVAJM= 87 | export WORKER_AUTH_KEY=uDuFypRqVPq7DM0Ujgrl5PoRRgqqy4eFdW7ORNIHHhU= 88 | export RECOVERY_KEY=eixF4zdao9C6Soe+5jzBTtCnLUGMsP72NW5x30QP6So= 89 | 90 | hashi-up boundary install \ 91 | --ssh-target-addr $CONTROLLER_IP \ 92 | --controller-name boundary-controller \ 93 | --db-url "postgresql://boundary:boundary123@localhost:5432/boundary?sslmode=disable" \ 94 | --root-key $ROOT_KEY \ 95 | --worker-auth-key $WORKER_AUTH_KEY \ 96 | --cluster-addr $CONTROLLER_IP \ 97 | --recovery-key $RECOVERY_KEY 98 | ``` 99 | 100 | And finally, install two Boundary Workers on the remaining nodes: 101 | 102 | ``` sh 103 | export CONTROLLER_IP=192.168.0.100 104 | export WORKER_01_IP=192.168.0.101 105 | export WORKER_02_IP=192.168.0.102 106 | export ROOT_KEY=JuZetUPcWEbsO4cTP7/E93O8Zrl6CQnYYb25CLFVAJM= 107 | export WORKER_AUTH_KEY=uDuFypRqVPq7DM0Ujgrl5PoRRgqqy4eFdW7ORNIHHhU= 108 | export RECOVERY_KEY=eixF4zdao9C6Soe+5jzBTtCnLUGMsP72NW5x30QP6So= 109 | 110 | hashi-up boundary install \ 111 | --ssh-target-addr $WORKER_01_IP \ 112 | --worker-name boundary-worker-01 \ 113 | --public-addr $WORKER_01_IP \ 114 | --worker-auth-key $WORKER_AUTH_KEY \ 115 | --controller $CONTROLLER_IP 116 | 117 | hashi-up boundary install \ 118 | --ssh-target-addr $WORKER2_PUBLIC_IP \ 119 | --worker-name boundary-worker-01 \ 120 | --public-addr $WORKER_02_IP \ 121 | --worker-auth-key $WORKER_AUTH_KEY \ 122 | --controller $CONTROLLER_IP 123 | ``` 124 | 125 | ## What happens during installation? 126 | 127 | During installation the following steps are executed on the target host 128 | 129 | - download the Boundary distribution from https://releases.hashicorp.com and place the binary in `/usr/local/bin` 130 | - create a `boundary` user and directories, like `/etc/boundary.d` and `/opt/boundary` 131 | - generate or upload the config file to `/etc/boundary.d/boundary.hcl` 132 | - upload other resources, like certificates, to `/etc/boundary.d` 133 | - create a systemd service file for Boundary 134 | - enable and start this new systemd service 135 | 136 | ## CLI options 137 | 138 | ```text 139 | $ hashi-up boundary install --help 140 | Usage: 141 | hashi-up boundary install [flags] 142 | 143 | Flags: 144 | --api-addr string Boundary: address for the API listener (default "0.0.0.0") 145 | --api-cert-file string Boundary: specifies the path to the certificate for TLS. 146 | --api-key-file string Boundary: specifies the path to the private key for the certificate. 147 | --cluster-addr string Boundary: address for the Cluster listener (default "127.0.0.1") 148 | --cluster-cert-file string Boundary: specifies the path to the certificate for TLS. 149 | --cluster-key-file string Boundary: specifies the path to the private key for the certificate. 150 | -c, --config-file string Custom Boundary configuration file to upload 151 | --controller stringArray Boundary: a list of hosts/IP addresses and optionally ports for reaching controllers. (default [127.0.0.1]) 152 | --controller-name string Boundary: specifies a unique name of this controller within the Boundary controller cluster. 153 | --db-url string Boundary: configures the URL for connecting to Postgres 154 | -f, --file stringArray Additional files, e.g. certificates, to upload 155 | -h, --help help for install 156 | --local Running the installation locally, without ssh 157 | --package string Upload and use this Boundary package instead of downloading 158 | --proxy-addr string Boundary: address for the Proxy listener (default "0.0.0.0") 159 | --proxy-cert-file string Boundary: specifies the path to the certificate for TLS. 160 | --proxy-key-file string Boundary: specifies the path to the private key for the certificate. 161 | --public-addr string Boundary: specifies the public host or IP address (and optionally port) at which the worker can be reached by clients for proxying. 162 | --public-cluster-addr string Boundary: specifies the public host or IP address (and optionally port) at which the controller can be reached by workers. 163 | --recovery-key string Boundary: KMS key is used for rescue/recovery operations that can be used by a client to authenticate almost any operation within Boundary. 164 | --root-key string Boundary: a KEK (Key Encrypting Key) for the scope-specific KEKs (also referred to as the scope's root key). 165 | --skip-enable If set to true will not enable or start Boundary service 166 | --skip-start If set to true will not start Boundary service 167 | -r, --ssh-target-addr string Remote SSH target address (e.g. 127.0.0.1:22 168 | -k, --ssh-target-key string The ssh key to use for SSH login 169 | -p, --ssh-target-password string The ssh password to use for SSH login 170 | -s, --ssh-target-sudo-pass string The ssh password to use for SSH login 171 | -u, --ssh-target-user string Username for SSH login (default "root") 172 | -v, --version string Version of Boundary to install 173 | --worker-auth-key string Boundary: KMS key shared by the Controller and Worker in order to authenticate a Worker to the Controller. 174 | --worker-name string Boundary: specifies a unique name of this worker within the Boundary worker cluster. 175 | ``` -------------------------------------------------------------------------------- /docs/consul.md: -------------------------------------------------------------------------------- 1 | # Installing Consul with hashi-up 2 | 3 | There are two ways to install Consul with `hashi-up`. 4 | 5 | By default, `hashi-up` will generate a Consul configuration file with values based on the CLI flags. 6 | Consul has a lot of configuration options, and `hashi-up` tries to provide the most convenient parts as CLI options. 7 | This allows you to install Consul without creating a configuration file. 8 | 9 | For more advanced use cases, use the `--config-file` flags to upload an existing configuration files, together with additional resources like certificates. 10 | 11 | ## Installing Consul 12 | 13 | ### Set up a single Consul server 14 | 15 | Provision a new VM running a compatible operating system such as Ubuntu, Debian, Raspbian, or something else. Make sure that you opt-in to copy your registered SSH keys over to the new VM or host automatically. 16 | 17 | > Note: You can copy ssh keys to a remote VM with `ssh-copy-id user@IP`. 18 | 19 | Imagine the IP was `192.168.0.100` and the username was `ubuntu`, then you would run this: 20 | 21 | ```sh 22 | export IP=192.168.0.100 23 | 24 | hashi-up consul install \ 25 | --ssh-target-addr $IP \ 26 | --ssh-target-user ubuntu \ 27 | --server \ 28 | --client-addr 0.0.0.0 29 | ``` 30 | 31 | When the command finishes, try to access Consul using the UI at http://192.168.100:8500 or with the cli: 32 | 33 | ``` 34 | consul members -http-addr=http://192.168.0.100:8500 35 | ``` 36 | 37 | ### Join some agents to your Consul server 38 | 39 | Now that you have a Consul server up and running, you can join one or more client agents to the cluster: 40 | 41 | ```sh 42 | export SERVER_IP=192.168.0.100 43 | export AGENT_1_IP=192.168.0.105 44 | export AGENT_2_IP=192.168.0.106 45 | 46 | hashi-up consul install \ 47 | --ssh-target-addr $AGENT_1_IP \ 48 | --ssh-target-user ubuntu \ 49 | --retry-join $SERVER_IP 50 | 51 | hashi-up consul install \ 52 | --ssh-target-addr $AGENT_2_IP \ 53 | --ssh-target-user ubuntu \ 54 | --retry-join $SERVER_IP 55 | ``` 56 | 57 | ### Create a multi-server (HA) setup 58 | 59 | Prepare, for example, 3 nodes and let's say the have the following ip addresses: 60 | 61 | - 192.168.0.100 62 | - 192.168.0.101 63 | - 192.168.0.102 64 | 65 | With `hashi-up` it is quite easy to install 3 Consul servers which will form a cluster: 66 | 67 | ```sh 68 | export SERVER_1_IP=192.168.0.100 69 | export SERVER_2_IP=192.168.0.101 70 | export SERVER_3_IP=192.168.0.102 71 | 72 | hashi-up consul install \ 73 | --ssh-target-addr $SERVER_1_IP \ 74 | --ssh-target-user ubuntu \ 75 | --server \ 76 | --client-addr 0.0.0.0 \ 77 | --bootstrap-expect 3 \ 78 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 79 | 80 | hashi-up consul install \ 81 | --ssh-target-addr $SERVER_2_IP \ 82 | --ssh-target-user ubuntu \ 83 | --server \ 84 | --client-addr 0.0.0.0 \ 85 | --bootstrap-expect 3 \ 86 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 87 | 88 | hashi-up consul install \ 89 | --ssh-target-addr $SERVER_3_IP \ 90 | --ssh-target-user ubuntu \ 91 | --server \ 92 | --client-addr 0.0.0.0 \ 93 | --bootstrap-expect 3 \ 94 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 95 | ``` 96 | 97 | And of course, joining client agents is the same as above: 98 | 99 | ```sh 100 | export SERVER_1_IP=192.168.0.100 101 | export SERVER_2_IP=192.168.0.101 102 | export SERVER_3_IP=192.168.0.102 103 | export AGENT_1_IP=192.168.0.105 104 | export AGENT_2_IP=192.168.0.106 105 | 106 | hashi-up consul install \ 107 | --ssh-target-addr $AGENT_1_IP \ 108 | --ssh-target-user ubuntu \ 109 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 110 | 111 | hashi-up consul install \ 112 | --ssh-target-addr $AGENT_2_IP \ 113 | --ssh-target-user ubuntu \ 114 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 115 | ``` 116 | 117 | ### Install Consul with an existing configuration file 118 | 119 | Perhaps you have already a configuration file available, or you want to configure Consul with some values which are not supported by the CLI options of `hashi-up`. 120 | In this case, use the `--config-file` and the `--file` options to upload this config file and additional resources. When doing so, all other CLI options are ignored. 121 | 122 | First create a config file and additional resources like certificates and keys, e.g. `server.hcl`: 123 | 124 | ```hcl 125 | datacenter = "dc1" 126 | data_dir = "/opt/consul" 127 | client_addr = "0.0.0.0" 128 | 129 | ports { 130 | grpc = 8502 131 | https = 8501 132 | http = -1 133 | } 134 | 135 | server = true 136 | bootstrap_expect = 3 137 | ca_file = "/etc/consul.d/ca.pem" 138 | cert_file = "/etc/consul.d/server.pem" 139 | key_file = "/etc/consul.d/server-key.pem" 140 | verify_incoming_rpc = true 141 | verify_outgoing = true 142 | verify_server_hostname = true 143 | 144 | connect { 145 | enabled = true 146 | } 147 | ``` 148 | 149 | Next, you can install Consul with those resources: 150 | 151 | ```sh 152 | export SERVER_IP=192.168.0.100 153 | 154 | hashi-up consul install \ 155 | --ssh-target-addr $MASTER_NODE_IP \ 156 | --ssh-target-user ubuntu \ 157 | --config-file ./server.hcl \ 158 | --file ./ca.pem \ 159 | --file ./server.pem \ 160 | --file ./server-key.pem 161 | ``` 162 | 163 | > Note that `hashi-up` will upload the additional resources to `/etc/consul.d` 164 | 165 | ## What happens during installation? 166 | 167 | During installation the following steps are executed on the target host 168 | 169 | - download the Consul distribution from https://releases.hashicorp.com and place the binary in `/usr/local/bin` 170 | - create a `consul` user and directories, like `/etc/consul.d` and `/opt/consul` 171 | - generate or upload the config file to `/etc/consul.d/consul.hcl` 172 | - upload other resources, like certificates, to `/etc/consul.d` 173 | - create a systemd service file for Consul 174 | - enable and start this new systemd service 175 | 176 | ## CLI options 177 | 178 | ```text 179 | $ hashi-up consul install --help 180 | Install Consul on a server via SSH 181 | 182 | Usage: 183 | hashi-up consul install [flags] 184 | 185 | Flags: 186 | --acl Consul: enables Consul ACL system. (see Consul documentation for more info) 187 | --advertise-addr string Consul: sets the advertise address to use. (see Consul documentation for more info) 188 | --agent-token string Consul: the token that the agent will use for internal agent operations.. (see Consul documentation for more info) 189 | --auto-encrypt Consul: this option enables auto_encrypt and allows servers to automatically distribute certificates from the Connect CA to the clients. (see Consul documentation for more info) 190 | --bind-addr string Consul: sets the bind address for cluster communication. (see Consul documentation for more info) 191 | --bootstrap-expect int Consul: sets server to expect bootstrap mode. 0 are less disables bootstrap mode. (see Consul documentation for more info) (default 1) 192 | --ca-file string Consul: the certificate authority used to check the authenticity of client and server connections. (see Consul documentation for more info) 193 | --cert-file string Consul: the certificate to verify the agent's authenticity. (see Consul documentation for more info) 194 | --client-addr string Consul: sets the address to bind for client access. (see Consul documentation for more info) 195 | -c, --config-file string Custom Consul configuration file to upload, setting this will disable config file generation meaning the other flags are ignored 196 | --connect Consul: enables the Connect feature on the agent. (see Consul documentation for more info) 197 | --datacenter string Consul: specifies the data center of the local agent. (see Consul documentation for more info) (default "dc1") 198 | --dns-addr string Consul: sets the address for the DNS server. (see Consul documentation for more info) 199 | --encrypt string Consul: provides the gossip encryption key. (see Consul documentation for more info) 200 | -f, --file strings Additional files, e.g. certificates, to upload 201 | --grpc-addr string Consul: sets the address for the gRPC API server. (see Consul documentation for more info) 202 | -h, --help help for install 203 | --http-addr string Consul: sets the address for the HTTP API server. (see Consul documentation for more info) 204 | --https-addr string Consul: sets the address for the HTTPS API server. (see Consul documentation for more info) 205 | --https-only Consul: if true, HTTP port is disabled on both clients and servers and to only accept HTTPS connections when TLS enabled. (default true) 206 | --key-file string Consul: the key used with the certificate to verify the agent's authenticity. (see Consul documentation for more info) 207 | --local Running the installation locally, without ssh 208 | --package string Upload and use this Consul package instead of downloading 209 | --retry-join strings Consul: address of an agent to join at start time with retries enabled. Can be specified multiple times. (see Consul documentation for more info) 210 | --server Consul: switches agent to server mode. (see Consul documentation for more info) 211 | --skip-enable If set to true will not enable or start Consul service 212 | --skip-start If set to true will not start Consul service 213 | -r, --ssh-target-addr string Remote SSH target address (e.g. 127.0.0.1:22 214 | -k, --ssh-target-key string The ssh key to use for SSH login 215 | -p, --ssh-target-password string The ssh password to use for SSH login 216 | -s, --ssh-target-sudo-pass string The ssh password to use for SSH login 217 | -u, --ssh-target-user string Username for SSH login (default "root") 218 | -v, --version string Version of Consul to install 219 | ``` -------------------------------------------------------------------------------- /docs/nomad.md: -------------------------------------------------------------------------------- 1 | # Installing Nomad with hashi-up 2 | 3 | There are two ways to install Nomad with `hashi-up`. 4 | 5 | By default, `hashi-up` will generate a Nomad configuration file with values based on the CLI flags. 6 | Nomad has a lot of configuration options, and `hashi-up` tries to provide the most convenient parts as CLI options. 7 | This allows you to install Nomad without creating a configuration file. 8 | 9 | For more advanced use cases, use the `--config-file` flags to upload an existing configuration files, together with additional resources like certificates. 10 | 11 | ## Installing Nomad 12 | 13 | ### Set up a single Nomad server 14 | 15 | Provision a new VM running a compatible operating system such as Ubuntu, Debian, Raspbian, or something else. Make sure that you opt-in to copy your registered SSH keys over to the new VM or host automatically. 16 | 17 | > Note: You can copy ssh keys to a remote VM with `ssh-copy-id user@IP`. 18 | 19 | Imagine the IP was `192.168.0.100` and the username was `ubuntu`, then you would run this: 20 | 21 | ```sh 22 | export IP=192.168.0.100 23 | hashi-up nomad install \ 24 | --ssh-target-addr $IP \ 25 | --ssh-target-user ubuntu \ 26 | --server 27 | ``` 28 | 29 | When the command finishes, try to access Nomad using the UI at http://192.168.100:4646 or with the cli: 30 | 31 | ```sh 32 | nomad agent-info -address=http://192.168.0.100:4646 33 | ``` 34 | 35 | ### Join some agents to your Nomad server 36 | 37 | Let's say you have a Nomad server up and running, now you can join one or more client agents to the cluster: 38 | 39 | ```sh 40 | export SERVER_IP=192.168.0.100 41 | export AGENT_1_IP=192.168.0.105 42 | export AGENT_2_IP=192.168.0.106 43 | 44 | hashi-up nomad install \ 45 | --ssh-target-addr $AGENT_1_IP \ 46 | --ssh-target-user ubuntu \ 47 | --client \ 48 | --retry-join $SERVER_IP 49 | 50 | hashi-up nomad install \ 51 | --ssh-target-addr $AGENT_2_IP \ 52 | --ssh-target-user ubuntu \ 53 | --client \ 54 | --retry-join $SERVER_IP 55 | ``` 56 | 57 | ### Create a multi-server (HA) setup 58 | 59 | Prepare, for example, 3 nodes and let's say the have the following ip addresses: 60 | 61 | - 192.168.0.100 62 | - 192.168.0.101 63 | - 192.168.0.102 64 | 65 | With `hashi-up` it is quite easy to install 3 Nomad servers which will form a cluster: 66 | 67 | ```sh 68 | export SERVER_1_IP=192.168.0.100 69 | export SERVER_2_IP=192.168.0.101 70 | export SERVER_3_IP=192.168.0.102 71 | 72 | hashi-up nomad install \ 73 | --ssh-target-addr $SERVER_1_IP \ 74 | --ssh-target-user ubuntu \ 75 | --server \ 76 | --bootstrap-expect 3 \ 77 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 78 | 79 | hashi-up nomad install \ 80 | --ssh-target-addr $SERVER_2_IP \ 81 | --ssh-target-user ubuntu \ 82 | --server \ 83 | --bootstrap-expect 3 \ 84 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 85 | 86 | hashi-up nomad install \ 87 | --ssh-target-addr $SERVER_3_IP \ 88 | --ssh-target-user ubuntu \ 89 | --server \ 90 | --bootstrap-expect 3 \ 91 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 92 | ``` 93 | 94 | And of course, joining client agents is the same as above: 95 | 96 | ```sh 97 | export SERVER_1_IP=192.168.0.100 98 | export SERVER_2_IP=192.168.0.101 99 | export SERVER_3_IP=192.168.0.102 100 | export AGENT_1_IP=192.168.0.105 101 | export AGENT_2_IP=192.168.0.106 102 | 103 | hashi-up nomad install \ 104 | --ssh-target-addr $AGENT_1_IP \ 105 | --ssh-target-user ubuntu \ 106 | --client \ 107 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 108 | 109 | hashi-up nomad install \ 110 | --ssh-target-addr $AGENT_2_IP \ 111 | --ssh-target-user ubuntu \ 112 | --client \ 113 | --retry-join $SERVER_1_IP --retry-join $SERVER_2_IP --retry-join $SERVER_3_IP 114 | ``` 115 | 116 | ### Create a multi-server (HA) setup with Consul 117 | 118 | If a Consul agent is already available on the Nomad nodes, Nomad can use Consul the automatically bootstrap the cluster. 119 | So after installing a Consul cluster on all nodes, with `hashi-up` the cluster as explained above can be installed with the following commands: 120 | 121 | ```sh 122 | export SERVER_1_IP=192.168.0.100 123 | export SERVER_2_IP=192.168.0.101 124 | export SERVER_3_IP=192.168.0.102 125 | export AGENT_1_IP=192.168.0.105 126 | export AGENT_2_IP=192.168.0.106 127 | 128 | hashi-up nomad install \ 129 | --ssh-target-addr $SERVER_1_IP \ 130 | --ssh-target-user ubuntu \ 131 | --server \ 132 | --bootstrap-expect 3 133 | 134 | hashi-up nomad install \ 135 | --ssh-target-addr $SERVER_2_IP \ 136 | --ssh-target-user ubuntu \ 137 | --server \ 138 | --bootstrap-expect 3 139 | 140 | hashi-up nomad install \ 141 | --ssh-target-addr $SERVER_3_IP \ 142 | --ssh-target-user ubuntu \ 143 | --server \ 144 | --bootstrap-expect 3 145 | 146 | hashi-up nomad install \ 147 | --ssh-target-addr $AGENT_1_IP \ 148 | --ssh-target-user ubuntu \ 149 | --client 150 | 151 | hashi-up nomad install \ 152 | --ssh-target-addr $AGENT_2_IP \ 153 | --ssh-target-user ubuntu \ 154 | --client 155 | ``` 156 | 157 | ### Install Nomad with an existing configuration file 158 | 159 | Perhaps you have already a configuration file available, or you want to configure Consul with some values which are not supported by the CLI options of `hashi-up`. 160 | In this case, use the `--config-file` and the `--file` options to upload this config file and additional resources. When doing so, all other CLI options are ignored. 161 | 162 | First create a config file and additional resources like certificates and keys, e.g. `server.hcl`: 163 | 164 | ```hcl 165 | data_dir = "/opt/nomad" 166 | 167 | # Enable the server 168 | server { 169 | enabled = true 170 | bootstrap_expect = 1 171 | } 172 | 173 | tls { 174 | http = true 175 | rpc = true 176 | 177 | ca_file = "/etc/nomad.d/nomad-ca.pem" 178 | cert_file = "/etc/nomad.d/server.pem" 179 | key_file = "/etc/nomad.d/server-key.pem" 180 | 181 | verify_server_hostname = true 182 | verify_https_client = true 183 | } 184 | ``` 185 | 186 | Next, you can install Nomad with those resources: 187 | 188 | ```sh 189 | export SERVER_IP=192.168.0.100 190 | 191 | hashi-up nomad install \ 192 | --ssh-target-addr $MASTER_NODE_IP \ 193 | --ssh-target-user ubuntu \ 194 | --config-file ./server.hcl \ 195 | --file ./nomad-ca.pem \ 196 | --file ./server.pem \ 197 | --file ./server-key.pem 198 | ``` 199 | 200 | > Note that `hashi-up` will upload the additional resources to `/etc/nomad.d` 201 | 202 | ## What happens during installation? 203 | 204 | During installation the following steps are executed on the target host 205 | 206 | - download the Nomad distribution from https://releases.hashicorp.com and place the binary in `/usr/local/bin` 207 | - create directories, like `/etc/nomad.d` and `/opt/nomad` 208 | - generate or upload the config file to `/etc/nomad.d/nomad.hcl` 209 | - upload other resources, like certificates, to `/etc/nomad.d` 210 | - create a systemd service file for Nomad 211 | - enable and start this new systemd service 212 | 213 | ## CLI options 214 | 215 | ```text 216 | $ hashi-up nomad install --help 217 | Install Nomad on a server via SSH 218 | 219 | Usage: 220 | hashi-up nomad install [flags] 221 | 222 | Flags: 223 | --acl Nomad: enables Nomad ACL system. (see Nomad documentation for more info) 224 | --address string Nomad: the address the agent will bind to for all of its various network services. (see Nomad documentation for more info) 225 | --advertise string Nomad: the address the agent will advertise to for all of its various network services. (see Nomad documentation for more info) 226 | --bootstrap-expect int Nomad: sets server to expect bootstrap mode. 0 are less disables bootstrap mode. (see Nomad documentation for more info) (default 1) 227 | --ca-file string Nomad: the certificate authority used to check the authenticity of client and server connections. (see Nomad documentation for more info) 228 | --cert-file string Nomad: the certificate to verify the agent's authenticity. (see Nomad documentation for more info) 229 | --client Nomad: enables the client mode of the agent. (see Nomad documentation for more info) 230 | -c, --config-file string Custom Nomad configuration file to upload, setting this will disable config file generation meaning the other flags are ignored 231 | --datacenter string Nomad: specifies the data center of the local agent. (see Nomad documentation for more info) (default "dc1") 232 | --encrypt string Nomad: Provides the gossip encryption key. (see Nomad documentation for more info) 233 | -f, --file strings Additional files, e.g. certificates, to upload 234 | -h, --help help for install 235 | --key-file string Nomad: the key used with the certificate to verify the agent's authenticity. (see Nomad documentation for more info) 236 | --local Running the installation locally, without ssh 237 | --node-class string Nomad: specifies an arbitrary string used to logically group client nodes by user-defined class. (see Nomad documentation for more info) 238 | --package string Upload and use this Nomad package instead of downloading 239 | --retry-join strings Nomad: address of an agent to join at start time with retries enabled. Can be specified multiple times. (see Nomad documentation for more info) 240 | --server Nomad: enables the server mode of the agent. (see Nomad documentation for more info) 241 | --skip-enable If set to true will not enable or start Nomad service 242 | --skip-start If set to true will not start Nomad service 243 | -r, --ssh-target-addr string Remote SSH target address (e.g. 127.0.0.1:22 244 | -k, --ssh-target-key string The ssh key to use for SSH login 245 | -p, --ssh-target-password string The ssh password to use for SSH login 246 | -s, --ssh-target-sudo-pass string The ssh password to use for SSH login 247 | -u, --ssh-target-user string Username for SSH login (default "root") 248 | -v, --version string Version of Nomad to install 249 | ``` 250 | -------------------------------------------------------------------------------- /docs/vault.md: -------------------------------------------------------------------------------- 1 | # Installing Vault with hashi-up 2 | 3 | There are two ways to install Vault with `hashi-up`. 4 | 5 | By default, `hashi-up` will generate a Vault configuration file with values based on the CLI flags. 6 | Vault has a lot of configuration options, and `hashi-up` tries to provide the most convenient parts as CLI options. 7 | This allows you to install Vault without creating a configuration file. 8 | 9 | For more advanced use cases, use the `--config-file` flags to upload an existing configuration files, together with additional resources like certificates. 10 | 11 | ## Installing Vault 12 | 13 | ### Set up a single Vault server with the Filesystem Storage Backend 14 | 15 | Provision a new VM running a compatible operating system such as Ubuntu, Debian, Raspbian, or something else. Make sure that you opt-in to copy your registered SSH keys over to the new VM or host automatically. 16 | 17 | > Note: You can copy ssh keys to a remote VM with `ssh-copy-id user@IP`. 18 | 19 | Imagine the IP was `192.168.0.100` and the username was `ubuntu`, then you would run this: 20 | 21 | ```sh 22 | export IP=192.168.0.100 23 | hashi-up vault install \ 24 | --ssh-target-addr $IP \ 25 | --ssh-target-user ubuntu \ 26 | --storage file 27 | ``` 28 | 29 | When the command finishes, try to access Vault using the UI at http://192.168.100:8200 or with the cli: 30 | 31 | ```sh 32 | vault status -address=http://192.168.100:8200 33 | ``` 34 | 35 | ### Create a multi-server (HA) setup 36 | 37 | Vault supports a multi-server mode for high availability. At this moment, `hashi-up` only support creating such a HA setup with the Consul Storage Backend. 38 | If you want to use other storage backends, you can always install Vault with a more advanced configuration file. 39 | 40 | Prepare, for example, 3 nodes and let's say the have the following ip addresses: 41 | 42 | - 192.168.0.100 43 | - 192.168.0.101 44 | - 192.168.0.102 45 | 46 | First install a Consul cluster on the nodes, as explained [here](consul.md). 47 | 48 | Next, with `hashi-up` it is quite easy to install 3 Vault servers which will form a cluster: 49 | 50 | ```sh 51 | export SERVER_1_IP=192.168.0.100 52 | export SERVER_2_IP=192.168.0.101 53 | export SERVER_3_IP=192.168.0.102 54 | 55 | hashi-up vault install \ 56 | --ssh-target-addr $SERVER_1_IP \ 57 | --ssh-target-user ubuntu \ 58 | --storage consul \ 59 | --api-addr http://$SERVER_1_IP:8200 60 | 61 | hashi-up vault install \ 62 | --ssh-target-addr $SERVER_2_IP \ 63 | --ssh-target-user ubuntu \ 64 | --storage consul \ 65 | --api-addr http://$SERVER_2_IP:8200 66 | 67 | hashi-up vault install \ 68 | --ssh-target-addr $SERVER_3_IP \ 69 | --ssh-target-user ubuntu \ 70 | --storage consul \ 71 | --api-addr http://$SERVER_3_IP:8200 72 | ``` 73 | 74 | ### Install Vault with an existing configuration file 75 | 76 | Perhaps you have already a configuration file available, or you want to configure Vault with some values which are not supported by the CLI options of `hashi-up`. 77 | In this case, use the `--config-file` and the `--file` options to upload this config file and additional resources. When doing so, all other CLI options are ignored. 78 | 79 | First create a config file and additional resources like certificates and keys, e.g. `server.hcl`: 80 | 81 | ```hcl 82 | ui = true 83 | listener "tcp" { 84 | address = "0.0.0.0:8200" 85 | tls_cert_file = "/etc/vault.d/server.pem" 86 | tls_key_file = "/etc/vault.d/server-key.pem" 87 | } 88 | 89 | api_addr = "https://vault-leader.my-company.internal" 90 | 91 | storage "gcs" { 92 | bucket = "mycompany-vault-data" 93 | ha_enabled = "true" 94 | } 95 | ``` 96 | 97 | Next, you can install Vault with those resources: 98 | 99 | ```sh 100 | export SERVER_IP=192.168.0.100 101 | 102 | hashi-up vault install \ 103 | --ssh-target-addr $MASTER_NODE_IP \ 104 | --ssh-target-user ubuntu \ 105 | --config-file ./server.hcl \ 106 | --file ./server.pem \ 107 | --file ./server-key.pem 108 | ``` 109 | 110 | > Note that `hashi-up` will upload the additional resources to `/etc/vault.d` 111 | 112 | ## What happens during installation? 113 | 114 | During installation the following steps are executed on the target host 115 | 116 | - download the Vault distribution from https://releases.hashicorp.com and place the binary in `/usr/local/bin` 117 | - create a `vault` user and directories, like `/etc/vault.d` and `/opt/vault` 118 | - generate or upload the config file to `/etc/vault.d/vault.hcl` 119 | - upload other resources, like certificates, to `/etc/vault.d` 120 | - create a systemd service file for Vault 121 | - enable and start this new systemd service 122 | 123 | ## CLI options 124 | 125 | ```text 126 | $ hashi-up vault install --help 127 | Install Vault on a server via SSH 128 | 129 | Usage: 130 | hashi-up vault install [flags] 131 | 132 | Flags: 133 | --address strings Vault: the address to bind to for listening. (see Vault documentation for more info) (default [0.0.0.0:8200]) 134 | --api-addr string Vault: the address (full URL) to advertise to other Vault servers in the cluster for client redirection. (see Vault documentation for more info) 135 | --cert-file string Vault: the certificate for TLS. (see Vault documentation for more info) 136 | --cluster-addr string Vault: the address to advertise to other Vault servers in the cluster for request forwarding. (see Vault documentation for more info) 137 | -c, --config-file string Custom Vault configuration file to upload, setting this will disable config file generation meaning the other flags are ignored 138 | --consul-addr string Vault: the address of the Consul agent to communicate with. (see Vault documentation for more info) (default "127.0.0.1:8500") 139 | --consul-path string Vault: the path in Consul's key-value store where Vault data will be stored. (see Vault documentation for more info) (default "vault/") 140 | --consul-tls-ca-file string Vault: the path to the CA certificate used for Consul communication. (see Vault documentation for more info) 141 | --consul-tls-cert-file string Vault: the path to the certificate for Consul communication. (see Vault documentation for more info) 142 | --consul-tls-key-file string Vault: the path to the private key for Consul communication. (see Vault documentation for more info) 143 | --consul-token string Vault: the Consul ACL token with permission to read and write from the path in Consul's key-value store. (see Vault documentation for more info) 144 | -f, --file strings Additional files, e.g. certificates, to upload 145 | -h, --help help for install 146 | --key-file string Vault: the private key for the certificate. (see Vault documentation for more info) 147 | --local Running the installation locally, without ssh 148 | --package string Upload and use this Vault package instead of downloading 149 | --skip-enable If set to true will not enable or start Vault service 150 | --skip-start If set to true will not start Vault service 151 | -r, --ssh-target-addr string Remote SSH target address (e.g. 127.0.0.1:22 152 | -k, --ssh-target-key string The ssh key to use for SSH login 153 | -p, --ssh-target-password string The ssh password to use for SSH login 154 | -s, --ssh-target-sudo-pass string The ssh password to use for SSH login 155 | -u, --ssh-target-user string Username for SSH login (default "root") 156 | --storage string Vault: the type of storage backend. Currently only "file" of "consul" is supported. (see Vault documentation for more info) (default "file") 157 | -v, --version string Version of Vault to install 158 | ``` -------------------------------------------------------------------------------- /examples/digitalocean/install_boundary.sh: -------------------------------------------------------------------------------- 1 | DB_PUBLIC_IP=$(terraform output -raw db_public_ip) 2 | DB_PRIVATE_IP=$(terraform output -raw db_private_ip) 3 | 4 | CONTROLLER_PUBLIC_IP=$(terraform output -raw controller_public_ip) 5 | CONTROLLER_PRIVATE_IP=$(terraform output -raw controller_private_ip) 6 | 7 | WORKER1_PUBLIC_IP=$(terraform output -raw worker1_public_ip) 8 | WORKER1_PRIVATE_IP=$(terraform output -raw worker1_private_ip) 9 | 10 | WORKER2_PUBLIC_IP=$(terraform output -raw worker2_public_ip) 11 | WORKER2_PRIVATE_IP=$(terraform output -raw worker2_private_ip) 12 | 13 | ROOT_KEY=JuZetUPcWEbsO4cTP7/E93O8Zrl6CQnYYb25CLFVAJM= 14 | WORKER_AUTH_KEY=uDuFypRqVPq7DM0Ujgrl5PoRRgqqy4eFdW7ORNIHHhU= 15 | RECOVERY_KEY=eixF4zdao9C6Soe+5jzBTtCnLUGMsP72NW5x30QP6So= 16 | 17 | hashi-up boundary init-database \ 18 | --ssh-target-addr $DB_PUBLIC_IP \ 19 | --db-url "postgresql://boundary:boundary123@localhost:5432/boundary?sslmode=disable" \ 20 | --root-key $ROOT_KEY 21 | 22 | hashi-up boundary install \ 23 | --ssh-target-addr $CONTROLLER_PUBLIC_IP \ 24 | --controller-name do-boundary-ctl \ 25 | --db-url "postgresql://boundary:boundary123@$DB_PRIVATE_IP:5432/boundary?sslmode=disable" \ 26 | --root-key $ROOT_KEY \ 27 | --worker-auth-key $WORKER_AUTH_KEY \ 28 | --cluster-addr $CONTROLLER_PRIVATE_IP \ 29 | --recovery-key $RECOVERY_KEY 30 | 31 | hashi-up boundary install \ 32 | --ssh-target-addr $WORKER1_PUBLIC_IP \ 33 | --worker-name do-boundary-wkr-1 \ 34 | --public-addr $WORKER1_PUBLIC_IP \ 35 | --worker-auth-key $WORKER_AUTH_KEY \ 36 | --controller $CONTROLLER_PRIVATE_IP 37 | 38 | hashi-up boundary install \ 39 | --ssh-target-addr $WORKER2_PUBLIC_IP \ 40 | --worker-name do-boundary-wkr-2 \ 41 | --public-addr $WORKER2_PUBLIC_IP \ 42 | --worker-auth-key $WORKER_AUTH_KEY \ 43 | --controller $CONTROLLER_PRIVATE_IP -------------------------------------------------------------------------------- /examples/digitalocean/main.tf: -------------------------------------------------------------------------------- 1 | resource "digitalocean_droplet" "db" { 2 | image = "docker-20-04" 3 | name = "boundary-db" 4 | region = var.region 5 | size = "s-1vcpu-1gb" 6 | vpc_uuid = var.vpc_uuid 7 | ssh_keys = [var.ssh_key_id] 8 | user_data = file("./scripts/setup_db.sh") 9 | } 10 | 11 | resource "digitalocean_droplet" "controller" { 12 | image = "ubuntu-20-04-x64" 13 | name = "boundary-controller" 14 | region = var.region 15 | size = "s-1vcpu-1gb" 16 | vpc_uuid = var.vpc_uuid 17 | ssh_keys = [var.ssh_key_id] 18 | user_data = file("./scripts/setup.sh") 19 | } 20 | 21 | resource "digitalocean_droplet" "worker1" { 22 | image = "ubuntu-20-04-x64" 23 | name = "boundary-worker-01" 24 | region = var.region 25 | size = "s-1vcpu-1gb" 26 | vpc_uuid = var.vpc_uuid 27 | ssh_keys = [var.ssh_key_id] 28 | user_data = file("./scripts/setup_db.sh") 29 | } 30 | 31 | resource "digitalocean_droplet" "worker2" { 32 | image = "ubuntu-20-04-x64" 33 | name = "boundary-worker-02" 34 | region = var.region 35 | size = "s-1vcpu-1gb" 36 | vpc_uuid = var.vpc_uuid 37 | ssh_keys = [var.ssh_key_id] 38 | user_data = file("./scripts/setup_db.sh") 39 | } 40 | 41 | output "db_public_ip" { 42 | value = digitalocean_droplet.db.ipv4_address 43 | } 44 | 45 | output "db_private_ip" { 46 | value = digitalocean_droplet.db.ipv4_address_private 47 | } 48 | 49 | output "controller_public_ip" { 50 | value = digitalocean_droplet.controller.ipv4_address 51 | } 52 | 53 | output "controller_private_ip" { 54 | value = digitalocean_droplet.controller.ipv4_address_private 55 | } 56 | 57 | output "worker1_public_ip" { 58 | value = digitalocean_droplet.worker1.ipv4_address 59 | } 60 | 61 | output "worker1_private_ip" { 62 | value = digitalocean_droplet.worker1.ipv4_address_private 63 | } 64 | 65 | output "worker2_public_ip" { 66 | value = digitalocean_droplet.worker2.ipv4_address 67 | } 68 | 69 | output "worker2_private_ip" { 70 | value = digitalocean_droplet.worker2.ipv4_address_private 71 | } -------------------------------------------------------------------------------- /examples/digitalocean/scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | apt update 4 | apt install unzip -y -------------------------------------------------------------------------------- /examples/digitalocean/scripts/setup_db.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | apt update 4 | apt install unzip -y 5 | docker run -d -p 5432:5432 -e POSTGRES_USER=boundary -e POSTGRES_PASSWORD=boundary123 -e POSTGRES_DB=boundary postgres:12 -------------------------------------------------------------------------------- /examples/digitalocean/variables.tf: -------------------------------------------------------------------------------- 1 | variable ssh_key_id { 2 | type = string 3 | } 4 | 5 | variable region { 6 | type = string 7 | } 8 | 9 | variable vpc_uuid { 10 | type = string 11 | } -------------------------------------------------------------------------------- /examples/digitalocean/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | digitalocean = { 4 | source = "digitalocean/digitalocean" 5 | } 6 | } 7 | required_version = ">= 1.0.0" 8 | } 9 | -------------------------------------------------------------------------------- /examples/multipass/consul/destroy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | multipass delete consul-server 4 | multipass delete consul-client-01 5 | multipass delete consul-client-02 6 | multipass delete consul-client-03 7 | multipass purge 8 | -------------------------------------------------------------------------------- /examples/multipass/consul/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PUBLIC_KEY=$(cat ${1:-~/.ssh/id_rsa.pub}) 4 | 5 | create_cloud_init() { 6 | cat < 0 { 90 | rootBody.SetAttributeValue("bootstrap_expect", cty.NumberIntVal(c.BootstrapExpect)) 91 | } 92 | } 93 | 94 | if len(c.Encrypt) != 0 { 95 | rootBody.SetAttributeValue("encrypt", cty.StringVal(c.Encrypt)) 96 | } 97 | 98 | if c.EnableTLS() { 99 | rootBody.SetAttributeValue("ca_file", cty.StringVal(makeAbsolute(c.CaFile, "/etc/consul.d"))) 100 | 101 | if c.Server || !c.AutoEncrypt { 102 | rootBody.SetAttributeValue("cert_file", cty.StringVal(makeAbsolute(c.CertFile, "/etc/consul.d"))) 103 | rootBody.SetAttributeValue("key_file", cty.StringVal(makeAbsolute(c.KeyFile, "/etc/consul.d"))) 104 | } 105 | 106 | rootBody.SetAttributeValue("verify_incoming_rpc", cty.BoolVal(true)) 107 | rootBody.SetAttributeValue("verify_outgoing", cty.BoolVal(true)) 108 | rootBody.SetAttributeValue("verify_server_hostname", cty.BoolVal(true)) 109 | 110 | if c.AutoEncrypt { 111 | autoTLSBlock := rootBody.AppendNewBlock("auto_encrypt", []string{}) 112 | 113 | if c.Server { 114 | autoTLSBlock.Body().SetAttributeValue("allow_tls", cty.BoolVal(true)) 115 | } else { 116 | rootBody.SetAttributeValue("verify_incoming_rpc", cty.BoolVal(false)) 117 | autoTLSBlock.Body().SetAttributeValue("tls", cty.BoolVal(true)) 118 | } 119 | } 120 | } 121 | 122 | if c.EnableACL { 123 | aclBlock := rootBody.AppendNewBlock("acl", []string{}) 124 | aclBlock.Body().SetAttributeValue("enabled", cty.BoolVal(true)) 125 | aclBlock.Body().SetAttributeValue("default_policy", cty.StringVal("deny")) 126 | aclBlock.Body().SetAttributeValue("down_policy", cty.StringVal("extend-cache")) 127 | aclBlock.Body().SetAttributeValue("enable_token_persistence", cty.BoolVal(true)) 128 | 129 | if len(c.AgentToken) != 0 { 130 | tokensBlock := aclBlock.Body().AppendNewBlock("tokens", []string{}) 131 | tokensBlock.Body().SetAttributeValue("agent", cty.StringVal(c.AgentToken)) 132 | } 133 | } 134 | 135 | if c.EnableConnect { 136 | connectBlock := rootBody.AppendNewBlock("connect", []string{}) 137 | connectBlock.Body().SetAttributeValue("enabled", cty.BoolVal(true)) 138 | } 139 | 140 | return generate(f) 141 | } 142 | -------------------------------------------------------------------------------- /pkg/config/functions.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/hashicorp/hcl/v2/hclwrite" 7 | "github.com/mitchellh/go-homedir" 8 | "github.com/zclconf/go-cty/cty" 9 | ) 10 | 11 | func transform(vs []string) []cty.Value { 12 | vsm := make([]cty.Value, len(vs)) 13 | for i, v := range vs { 14 | vsm[i] = cty.StringVal(v) 15 | } 16 | return vsm 17 | } 18 | 19 | func makeAbsolute(path string, base string) string { 20 | _, filename := filepath.Split(expandPath(path)) 21 | return base + "/" + filename 22 | } 23 | 24 | func expandPath(path string) string { 25 | res, _ := homedir.Expand(path) 26 | return res 27 | } 28 | 29 | func generate(f *hclwrite.File) string { 30 | str := `# generated with hashi-up 31 | 32 | ` 33 | return str + string(f.Bytes()) 34 | } 35 | -------------------------------------------------------------------------------- /pkg/config/nomad.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2/hclwrite" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | type NomadConfig struct { 9 | Datacenter string 10 | BindAddr string 11 | AdvertiseAddr string 12 | Server bool 13 | Client bool 14 | NodeClass string 15 | BootstrapExpect int64 16 | RetryJoin []string 17 | Encrypt string 18 | CaFile string 19 | CertFile string 20 | KeyFile string 21 | EnableACL bool 22 | } 23 | 24 | func (c NomadConfig) EnableTLS() bool { 25 | return len(c.CaFile) != 0 && len(c.CertFile) != 0 && len(c.KeyFile) != 0 26 | } 27 | 28 | func (c NomadConfig) GenerateConfigFile() string { 29 | 30 | f := hclwrite.NewEmptyFile() 31 | rootBody := f.Body() 32 | 33 | rootBody.SetAttributeValue("datacenter", cty.StringVal(c.Datacenter)) 34 | rootBody.SetAttributeValue("data_dir", cty.StringVal("/opt/nomad")) 35 | 36 | if len(c.BindAddr) != 0 { 37 | addressesBlock := rootBody.AppendNewBlock("addresses", []string{}) 38 | addressesBlock.Body().SetAttributeValue("http", cty.StringVal(c.BindAddr)) 39 | addressesBlock.Body().SetAttributeValue("rpc", cty.StringVal(c.BindAddr)) 40 | addressesBlock.Body().SetAttributeValue("serf", cty.StringVal(c.BindAddr)) 41 | } 42 | 43 | if len(c.AdvertiseAddr) != 0 { 44 | addressesBlock := rootBody.AppendNewBlock("advertise", []string{}) 45 | addressesBlock.Body().SetAttributeValue("http", cty.StringVal(c.AdvertiseAddr)) 46 | addressesBlock.Body().SetAttributeValue("rpc", cty.StringVal(c.AdvertiseAddr)) 47 | addressesBlock.Body().SetAttributeValue("serf", cty.StringVal(c.AdvertiseAddr)) 48 | } 49 | 50 | if c.Server { 51 | serverBlock := rootBody.AppendNewBlock("server", []string{}) 52 | serverBlock.Body().SetAttributeValue("enabled", cty.BoolVal(true)) 53 | if c.BootstrapExpect > 0 { 54 | serverBlock.Body().SetAttributeValue("bootstrap_expect", cty.NumberIntVal(c.BootstrapExpect)) 55 | } 56 | 57 | if len(c.RetryJoin) != 0 { 58 | serverJoinBlock := serverBlock.Body().AppendNewBlock("server_join", []string{}) 59 | serverJoinBlock.Body().SetAttributeValue("retry_join", cty.ListVal(transform(c.RetryJoin))) 60 | } 61 | 62 | if len(c.Encrypt) != 0 { 63 | serverBlock.Body().SetAttributeValue("encrypt", cty.StringVal(c.Encrypt)) 64 | } 65 | } 66 | 67 | if c.Client { 68 | clientBlock := rootBody.AppendNewBlock("client", []string{}) 69 | clientBlock.Body().SetAttributeValue("enabled", cty.BoolVal(true)) 70 | 71 | if len(c.NodeClass) != 0 { 72 | clientBlock.Body().SetAttributeValue("node_class", cty.StringVal(c.NodeClass)) 73 | } 74 | 75 | if len(c.RetryJoin) != 0 { 76 | serverJoinBlock := clientBlock.Body().AppendNewBlock("server_join", []string{}) 77 | serverJoinBlock.Body().SetAttributeValue("retry_join", cty.ListVal(transform(c.RetryJoin))) 78 | } 79 | } 80 | 81 | if c.EnableTLS() { 82 | tlsBlock := rootBody.AppendNewBlock("tls", []string{}) 83 | tlsBlock.Body().SetAttributeValue("http", cty.BoolVal(true)) 84 | tlsBlock.Body().SetAttributeValue("rpc", cty.BoolVal(true)) 85 | tlsBlock.Body().SetAttributeValue("ca_file", cty.StringVal(makeAbsolute(c.CaFile, "/etc/nomad.d"))) 86 | tlsBlock.Body().SetAttributeValue("cert_file", cty.StringVal(makeAbsolute(c.CertFile, "/etc/nomad.d"))) 87 | tlsBlock.Body().SetAttributeValue("key_file", cty.StringVal(makeAbsolute(c.KeyFile, "/etc/nomad.d"))) 88 | } 89 | 90 | if c.EnableACL { 91 | aclBlock := rootBody.AppendNewBlock("acl", []string{}) 92 | aclBlock.Body().SetAttributeValue("enabled", cty.BoolVal(true)) 93 | } 94 | 95 | return generate(f) 96 | } 97 | -------------------------------------------------------------------------------- /pkg/config/vault.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/hashicorp/hcl/v2/hclwrite" 5 | "github.com/zclconf/go-cty/cty" 6 | ) 7 | 8 | type VaultConfig struct { 9 | ApiAddr string 10 | ClusterAddr string 11 | Address []string 12 | CertFile string 13 | KeyFile string 14 | Storage string 15 | ConsulAddr string 16 | ConsulPath string 17 | ConsulToken string 18 | ConsulCaFile string 19 | ConsulCertFile string 20 | ConsulKeyFile string 21 | } 22 | 23 | func (c VaultConfig) EnableTLS() bool { 24 | return len(c.CertFile) != 0 && len(c.KeyFile) != 0 25 | } 26 | 27 | func (c VaultConfig) EnableConsulTLS() bool { 28 | return len(c.ConsulCaFile) != 0 && len(c.ConsulCertFile) != 0 && len(c.ConsulKeyFile) != 0 29 | } 30 | 31 | func (c VaultConfig) GenerateConfigFile() string { 32 | f := hclwrite.NewEmptyFile() 33 | rootBody := f.Body() 34 | 35 | rootBody.SetAttributeValue("ui", cty.BoolVal(true)) 36 | 37 | storageBlock := rootBody.AppendNewBlock("storage", []string{c.Storage}) 38 | 39 | if c.Storage == "file" { 40 | storageBlock.Body().SetAttributeValue("path", cty.StringVal("/opt/vault")) 41 | } 42 | 43 | if c.Storage == "consul" { 44 | storageBlock.Body().SetAttributeValue("address", cty.StringVal(c.ConsulAddr)) 45 | storageBlock.Body().SetAttributeValue("path", cty.StringVal(c.ConsulPath)) 46 | 47 | if len(c.ConsulToken) != 0 { 48 | storageBlock.Body().SetAttributeValue("token", cty.StringVal(c.ConsulToken)) 49 | } 50 | 51 | if c.EnableConsulTLS() { 52 | storageBlock.Body().SetAttributeValue("scheme", cty.StringVal("https")) 53 | storageBlock.Body().SetAttributeValue("tls_ca_file", cty.StringVal(makeAbsolute(c.ConsulCaFile, "/etc/vault.d"))) 54 | storageBlock.Body().SetAttributeValue("tls_cert_file", cty.StringVal(makeAbsolute(c.ConsulCertFile, "/etc/vault.d"))) 55 | storageBlock.Body().SetAttributeValue("tls_key_file", cty.StringVal(makeAbsolute(c.ConsulKeyFile, "/etc/vault.d"))) 56 | } 57 | } 58 | 59 | if len(c.ApiAddr) != 0 { 60 | rootBody.SetAttributeValue("api_addr", cty.StringVal(c.ApiAddr)) 61 | } 62 | 63 | if len(c.ClusterAddr) != 0 { 64 | rootBody.SetAttributeValue("cluster_addr", cty.StringVal(c.ClusterAddr)) 65 | } 66 | 67 | for _, a := range c.Address { 68 | listenerBlock := rootBody.AppendNewBlock("listener", []string{"tcp"}) 69 | listenerBlock.Body().SetAttributeValue("address", cty.StringVal(a)) 70 | 71 | if c.EnableTLS() { 72 | listenerBlock.Body().SetAttributeValue("tls_disable", cty.BoolVal(false)) 73 | listenerBlock.Body().SetAttributeValue("tls_cert_file", cty.StringVal(makeAbsolute(c.CertFile, "/etc/vault.d"))) 74 | listenerBlock.Body().SetAttributeValue("tls_key_file", cty.StringVal(makeAbsolute(c.KeyFile, "/etc/vault.d"))) 75 | } else { 76 | listenerBlock.Body().SetAttributeValue("tls_disable", cty.BoolVal(true)) 77 | } 78 | } 79 | 80 | return generate(f) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/config/version.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "runtime" 9 | "time" 10 | 11 | "github.com/Masterminds/semver" 12 | ) 13 | 14 | type Product struct { 15 | } 16 | 17 | type Version struct { 18 | Version string `json:"version"` 19 | } 20 | 21 | func GetLatestVersion(product string) (string, error) { 22 | url := fmt.Sprintf("https://api.releases.hashicorp.com/v1/releases/%s?license_class=oss", product) 23 | 24 | client := http.Client{ 25 | Timeout: time.Second * 2, 26 | } 27 | 28 | req, err := http.NewRequest(http.MethodGet, url, nil) 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | res, err := client.Do(req) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | if res.StatusCode != 200 { 39 | return "", fmt.Errorf("invalid response code %d", res.StatusCode) 40 | } 41 | 42 | if res.Body != nil { 43 | defer res.Body.Close() 44 | } 45 | 46 | body, err := ioutil.ReadAll(res.Body) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | var result []Version 52 | if err := json.Unmarshal(body, &result); err != nil { 53 | return "", err 54 | } 55 | 56 | for _, i := range result { 57 | v, err := semver.NewVersion(i.Version) 58 | if err == nil && len(v.Metadata()) == 0 && len(v.Prerelease()) == 0 { 59 | return i.Version, nil 60 | } 61 | } 62 | 63 | return "", fmt.Errorf("unable to find latest version of %s", product) 64 | } 65 | 66 | func GetDownloadURL(product, arch string, version *semver.Version) string { 67 | v := semver.MustParse("1.10.4") 68 | 69 | if arch == "arm" && product == "consul" && version.LessThan(v) { 70 | arch = "armhfv6" 71 | } 72 | 73 | return fmt.Sprintf("https://releases.hashicorp.com/%s/%s/%s_%s_%s_%s.zip", product, version, product, version, runtime.GOOS, arch) 74 | } 75 | 76 | func GetArmSuffix(product string, version string) string { 77 | if product == "consul" { 78 | m := semver.MustParse("1.10.4") 79 | v := semver.MustParse(version) 80 | if v.LessThan(m) { 81 | return "armhfv6" 82 | } 83 | } 84 | return "arm" 85 | } 86 | -------------------------------------------------------------------------------- /pkg/operator/errors.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import "fmt" 4 | 5 | type TargetConnectError struct { 6 | reason error 7 | } 8 | 9 | func NewTargetConnectError(message error) *TargetConnectError { 10 | return &TargetConnectError{ 11 | reason: message, 12 | } 13 | } 14 | func (e *TargetConnectError) Error() string { 15 | return fmt.Sprintf("%s", e.reason) 16 | } 17 | 18 | type SshAgentError struct { 19 | reason error 20 | } 21 | 22 | func NewSshAgentError(message error) *SshAgentError { 23 | return &SshAgentError{ 24 | reason: message, 25 | } 26 | } 27 | func (e *SshAgentError) Error() string { 28 | return fmt.Sprintf("%s", e.reason) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/operator/local.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "strconv" 7 | 8 | goexecute "github.com/alexellis/go-execute/pkg/v1" 9 | ) 10 | 11 | type LocalOperator struct { 12 | } 13 | 14 | func NewLocalOperator() *LocalOperator { 15 | return &LocalOperator{} 16 | } 17 | 18 | func (e LocalOperator) Execute(command string) error { 19 | task := goexecute.ExecTask{ 20 | Command: command, 21 | Shell: true, 22 | StreamStdio: true, 23 | } 24 | 25 | _, err := task.Execute() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (e LocalOperator) UploadFile(path string, remotePath string, mode string) error { 34 | source, err := os.Open(expandPath(path)) 35 | if err != nil { 36 | return err 37 | } 38 | defer source.Close() 39 | 40 | return e.Upload(source, remotePath, mode) 41 | } 42 | 43 | func (e LocalOperator) Upload(source io.Reader, remotePath string, mode string) error { 44 | permissions, err := strconv.ParseInt(mode, 8, 32) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | destination, err := os.OpenFile(remotePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.FileMode(permissions)) 50 | if err != nil { 51 | return err 52 | } 53 | defer destination.Close() 54 | 55 | _, err = io.Copy(destination, source) 56 | 57 | return err 58 | } 59 | -------------------------------------------------------------------------------- /pkg/operator/operator.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "os" 10 | "strings" 11 | 12 | "github.com/mitchellh/go-homedir" 13 | "github.com/pkg/errors" 14 | "golang.org/x/crypto/ssh" 15 | "golang.org/x/crypto/ssh/agent" 16 | "golang.org/x/crypto/ssh/terminal" 17 | ) 18 | 19 | type CommandRes struct { 20 | StdOut []byte 21 | StdErr []byte 22 | } 23 | 24 | type CommandOperator interface { 25 | Execute(command string) error 26 | Upload(src io.Reader, remotePath string, mode string) error 27 | UploadFile(path string, remotePath string, mode string) error 28 | } 29 | 30 | type Callback func(CommandOperator) error 31 | 32 | func ExecuteLocal(callback Callback) error { 33 | return callback(NewLocalOperator()) 34 | } 35 | 36 | func ExecuteRemote(host string, user string, privateKey string, password string, callback Callback) error { 37 | var method ssh.AuthMethod 38 | 39 | if password != "" { 40 | method = ssh.Password(password) 41 | } else if privateKey == "" { 42 | sshAgentConn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) 43 | 44 | if err != nil { 45 | return NewSshAgentError(err) 46 | } 47 | 48 | defer sshAgentConn.Close() 49 | 50 | client := agent.NewClient(sshAgentConn) 51 | list, err := client.List() 52 | 53 | if err != nil || len(list) == 0 { 54 | return NewSshAgentError(err) 55 | } 56 | 57 | method = ssh.PublicKeysCallback(client.Signers) 58 | } else { 59 | buffer, err := ioutil.ReadFile(expandPath(privateKey)) 60 | if err != nil { 61 | return errors.Wrapf(err, "unable to parse private key: %s", privateKey) 62 | } 63 | 64 | key, err := ssh.ParsePrivateKey(buffer) 65 | 66 | if err != nil { 67 | if err.Error() != "ssh: this private key is passphrase protected" { 68 | return errors.Wrapf(err, "unable to parse private key: %s", privateKey) 69 | } 70 | 71 | sshAgent, closeAgent := privateKeyUsingSSHAgent(privateKey + ".pub") 72 | defer closeAgent() 73 | 74 | if sshAgent != nil { 75 | method = sshAgent 76 | } else { 77 | fmt.Printf("Enter passphrase for '%s': ", privateKey) 78 | STDIN := int(os.Stdin.Fd()) 79 | bytePassword, _ := terminal.ReadPassword(STDIN) 80 | fmt.Println() 81 | 82 | key, err = ssh.ParsePrivateKeyWithPassphrase(buffer, bytePassword) 83 | if err != nil { 84 | return errors.Wrapf(err, "parse private key with passphrase failed: %s", privateKey) 85 | } 86 | method = ssh.PublicKeys(key) 87 | } 88 | } else { 89 | method = ssh.PublicKeys(key) 90 | } 91 | } 92 | 93 | return executeRemote(host, user, method, callback) 94 | } 95 | 96 | func privateKeyUsingSSHAgent(publicKeyPath string) (ssh.AuthMethod, func() error) { 97 | if sshAgentConn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { 98 | sshAgent := agent.NewClient(sshAgentConn) 99 | 100 | signers, err := sshAgent.Signers() 101 | if err != nil || len(signers) == 0 { 102 | return nil, sshAgentConn.Close 103 | } 104 | 105 | pubkey, err := ioutil.ReadFile(expandPath(publicKeyPath)) 106 | if err != nil { 107 | return nil, sshAgentConn.Close 108 | } 109 | 110 | authkey, _, _, _, err := ssh.ParseAuthorizedKey(pubkey) 111 | if err != nil { 112 | return nil, sshAgentConn.Close 113 | } 114 | parsedkey := authkey.Marshal() 115 | 116 | for _, signer := range signers { 117 | if bytes.Equal(signer.PublicKey().Marshal(), parsedkey) { 118 | return ssh.PublicKeys(signer), sshAgentConn.Close 119 | } 120 | } 121 | } 122 | return nil, func() error { return nil } 123 | } 124 | 125 | func executeRemote(address string, user string, authMethod ssh.AuthMethod, callback Callback) error { 126 | 127 | host, port, err := net.SplitHostPort(address) 128 | if err != nil { 129 | if strings.Contains(err.Error(), "missing port") { 130 | host = address 131 | port = "22" 132 | } else { 133 | return fmt.Errorf("error splitting host/port: %w", err) 134 | } 135 | } 136 | 137 | config := &ssh.ClientConfig{ 138 | User: user, 139 | Auth: []ssh.AuthMethod{ 140 | authMethod, 141 | }, 142 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 143 | } 144 | operator, err := NewSSHOperator(net.JoinHostPort(host, port), config) 145 | 146 | if err != nil { 147 | return NewTargetConnectError(err) 148 | } 149 | 150 | defer operator.Close() 151 | 152 | return callback(operator) 153 | } 154 | 155 | func expandPath(path string) string { 156 | res, _ := homedir.Expand(path) 157 | return res 158 | } 159 | -------------------------------------------------------------------------------- /pkg/operator/ssh.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "context" 5 | "github.com/bramvdbogaerde/go-scp" 6 | "io" 7 | "os" 8 | 9 | "golang.org/x/crypto/ssh" 10 | ) 11 | 12 | type SSHOperator struct { 13 | conn *ssh.Client 14 | } 15 | 16 | func NewSSHOperator(address string, config *ssh.ClientConfig) (*SSHOperator, error) { 17 | conn, err := ssh.Dial("tcp", address, config) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | operator := SSHOperator{ 23 | conn: conn, 24 | } 25 | 26 | return &operator, nil 27 | } 28 | 29 | func (s SSHOperator) Close() error { 30 | return s.conn.Close() 31 | } 32 | 33 | func (s SSHOperator) Execute(command string) error { 34 | sess, err := s.conn.NewSession() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | defer sess.Close() 40 | 41 | sess.Stdout = os.Stdout 42 | sess.Stderr = os.Stderr 43 | err = sess.Run(command) 44 | 45 | return err 46 | } 47 | 48 | func (s SSHOperator) Upload(source io.Reader, remotePath string, mode string) error { 49 | sess, err := s.conn.NewSession() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | defer sess.Close() 55 | 56 | client := scp.Client{ 57 | Session: sess, 58 | Conn: s.conn, 59 | RemoteBinary: "scp", 60 | } 61 | 62 | err = client.CopyFile(context.Background(), source, remotePath, mode) 63 | 64 | return err 65 | } 66 | 67 | func (s SSHOperator) UploadFile(path string, remotePath string, mode string) error { 68 | source, err := os.Open(expandPath(path)) 69 | if err != nil { 70 | return err 71 | } 72 | defer source.Close() 73 | 74 | return s.Upload(source, remotePath, mode) 75 | } 76 | -------------------------------------------------------------------------------- /scripts/install_boundary.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | info() { 5 | echo '[INFO] ->' "$@" 6 | } 7 | 8 | fatal() { 9 | echo '[ERROR] ->' "$@" 10 | exit 1 11 | } 12 | 13 | verify_system() { 14 | if ! [ -d /run/systemd ]; then 15 | fatal 'Can not find systemd to use as a process supervisor for Boundary' 16 | fi 17 | } 18 | 19 | setup_env() { 20 | SUDO=sudo 21 | if [ "$(id -u)" -eq 0 ]; then 22 | SUDO= 23 | else 24 | if [ ! -z "$SUDO_PASS" ]; then 25 | echo $SUDO_PASS | sudo -S true 26 | echo "" 27 | fi 28 | fi 29 | 30 | BOUNDARY_DATA_DIR=/opt/boundary 31 | BOUNDARY_CONFIG_DIR=/etc/boundary.d 32 | BOUNDARY_SERVICE_FILE=/etc/systemd/system/boundary.service 33 | 34 | BIN_DIR=/usr/local/bin 35 | 36 | PRE_INSTALL_HASHES=$(get_installed_hashes) 37 | 38 | TMP_DIR={{.TmpDir}} 39 | SKIP_ENABLE={{.SkipEnable}} 40 | SKIP_START={{.SkipStart}} 41 | BOUNDARY_VERSION={{.Version}} 42 | 43 | cd $TMP_DIR 44 | } 45 | 46 | # --- set arch and suffix, fatal if architecture not supported --- 47 | setup_verify_arch() { 48 | if [ -z "$ARCH" ]; then 49 | ARCH=$(uname -m) 50 | fi 51 | case $ARCH in 52 | amd64) 53 | SUFFIX=amd64 54 | ;; 55 | x86_64) 56 | SUFFIX=amd64 57 | ;; 58 | arm64) 59 | SUFFIX=arm64 60 | ;; 61 | aarch64) 62 | SUFFIX=arm64 63 | ;; 64 | arm*) 65 | SUFFIX=arm 66 | ;; 67 | *) 68 | fatal "Unsupported architecture $ARCH" 69 | ;; 70 | esac 71 | } 72 | 73 | # --- get hashes of the current boundary bin and service files 74 | get_installed_hashes() { 75 | $SUDO sha256sum ${BIN_DIR}/boundary ${BOUNDARY_CONFIG_DIR}/* ${BOUNDARY_SERVICE_FILE} 2>&1 || true 76 | } 77 | 78 | has_yum() { 79 | [ -n "$(command -v yum)" ] 80 | } 81 | 82 | has_apt_get() { 83 | [ -n "$(command -v apt-get)" ] 84 | } 85 | 86 | install_dependencies() { 87 | if [ ! -x "${TMP_DIR}/boundary" ]; then 88 | if ! [ -x "$(command -v unzip)" ] || ! [ -x "$(command -v curl)" ]; then 89 | if $(has_apt_get); then 90 | $SUDO apt-get install -y curl unzip 91 | elif $(has_yum); then 92 | $SUDO yum install -y curl unzip 93 | else 94 | fatal "Could not find apt-get or yum. Cannot install dependencies on this OS" 95 | exit 1 96 | fi 97 | fi 98 | fi 99 | } 100 | 101 | download_and_install() { 102 | if [ -f "${TMP_DIR}/boundary.zip" ]; then 103 | info "Installing uploaded Boundary package" 104 | $SUDO unzip -qq -o "$TMP_DIR/boundary.zip" -d $BIN_DIR 105 | else 106 | if [ -x "${BIN_DIR}/boundary" ] && [ "$(${BIN_DIR}/boundary version | grep "Version Number" | tr -s ' ' | cut -d' ' -f4)" = "${BOUNDARY_VERSION}" ]; then 107 | info "Boundary binary already installed in ${BIN_DIR}, skipping downloading and installing binary" 108 | else 109 | info "Downloading boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 110 | curl -o "$TMP_DIR/boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" -sfL "https://releases.hashicorp.com/boundary/${BOUNDARY_VERSION}/boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 111 | 112 | info "Downloading boundary_${BOUNDARY_VERSION}_SHA256SUMS" 113 | curl -o "$TMP_DIR/boundary_${BOUNDARY_VERSION}_SHA256SUMS" -sfL "https://releases.hashicorp.com/boundary/${BOUNDARY_VERSION}/boundary_${BOUNDARY_VERSION}_SHA256SUMS" 114 | info "Verifying downloaded boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 115 | sed -ni '/linux_'"${SUFFIX}"'.zip/p' "$TMP_DIR/boundary_${BOUNDARY_VERSION}_SHA256SUMS" 116 | sha256sum -c "$TMP_DIR/boundary_${BOUNDARY_VERSION}_SHA256SUMS" 117 | 118 | info "Unpacking boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 119 | $SUDO unzip -qq -o "$TMP_DIR/boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" -d $BIN_DIR 120 | fi 121 | fi 122 | } 123 | 124 | create_user_and_config() { 125 | if $(id boundary >/dev/null 2>&1); then 126 | info "User 'boundary' already exists, will not create again" 127 | else 128 | info "Creating user named 'boundary'" 129 | $SUDO useradd --system --home ${BOUNDARY_CONFIG_DIR} --shell /bin/false boundary 130 | fi 131 | 132 | $SUDO mkdir --parents ${BOUNDARY_DATA_DIR} 133 | $SUDO mkdir --parents ${BOUNDARY_CONFIG_DIR} 134 | 135 | if [ "$(ls -A ${TMP_DIR}/config/)" ]; then 136 | info "Copying configuration files" 137 | $SUDO cp ${TMP_DIR}/config/* ${BOUNDARY_CONFIG_DIR} 138 | fi 139 | 140 | $SUDO chown --recursive boundary:boundary /opt/boundary 141 | $SUDO chown --recursive boundary:boundary /etc/boundary.d 142 | } 143 | 144 | # --- write systemd service file --- 145 | create_systemd_service_file() { 146 | info "Adding system service file ${BOUNDARY_SERVICE_FILE}" 147 | $SUDO tee ${BOUNDARY_SERVICE_FILE} >/dev/null </dev/null 174 | $SUDO systemctl daemon-reload >/dev/null 175 | 176 | [ "${SKIP_START}" = true ] && return 177 | 178 | POST_INSTALL_HASHES=$(get_installed_hashes) 179 | if [ "${PRE_INSTALL_HASHES}" = "${POST_INSTALL_HASHES}" ]; then 180 | info "No change detected so skipping service start" 181 | return 182 | fi 183 | 184 | info "Starting systemd service" 185 | $SUDO systemctl restart boundary 186 | 187 | return 0 188 | } 189 | 190 | setup_env 191 | setup_verify_arch 192 | verify_system 193 | install_dependencies 194 | create_user_and_config 195 | download_and_install 196 | create_systemd_service_file 197 | systemd_enable_and_start 198 | -------------------------------------------------------------------------------- /scripts/install_boundary_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | info() { 5 | echo '[INFO] ->' "$@" 6 | } 7 | 8 | fatal() { 9 | echo '[ERROR] ->' "$@" 10 | exit 1 11 | } 12 | 13 | setup_env() { 14 | SUDO=sudo 15 | if [ "$(id -u)" -eq 0 ]; then 16 | SUDO= 17 | else 18 | if [ ! -z "$SUDO_PASS" ]; then 19 | echo $SUDO_PASS | sudo -S true 20 | echo "" 21 | fi 22 | fi 23 | 24 | BIN_DIR=/usr/local/bin 25 | 26 | TMP_DIR={{.TmpDir}} 27 | BOUNDARY_VERSION={{.Version}} 28 | 29 | cd $TMP_DIR 30 | } 31 | 32 | # --- set arch and suffix, fatal if architecture not supported --- 33 | setup_verify_arch() { 34 | if [ -z "$ARCH" ]; then 35 | ARCH=$(uname -m) 36 | fi 37 | case $ARCH in 38 | amd64) 39 | SUFFIX=amd64 40 | ;; 41 | x86_64) 42 | SUFFIX=amd64 43 | ;; 44 | arm64) 45 | SUFFIX=arm64 46 | ;; 47 | aarch64) 48 | SUFFIX=arm64 49 | ;; 50 | arm*) 51 | SUFFIX=arm 52 | ;; 53 | *) 54 | fatal "Unsupported architecture $ARCH" 55 | ;; 56 | esac 57 | } 58 | 59 | has_yum() { 60 | [ -n "$(command -v yum)" ] 61 | } 62 | 63 | has_apt_get() { 64 | [ -n "$(command -v apt-get)" ] 65 | } 66 | 67 | install_dependencies() { 68 | if [ ! -x "${TMP_DIR}/boundary" ]; then 69 | if ! [ -x "$(command -v unzip)" ] || ! [ -x "$(command -v curl)" ]; then 70 | if $(has_apt_get); then 71 | $SUDO apt-get install -y curl unzip 72 | elif $(has_yum); then 73 | $SUDO yum install -y curl unzip 74 | else 75 | fatal "Could not find apt-get or yum. Cannot install dependencies on this OS" 76 | exit 1 77 | fi 78 | fi 79 | fi 80 | } 81 | 82 | download_and_install() { 83 | if [ -f "${TMP_DIR}/boundary.zip" ]; then 84 | info "Installing uploaded Boundary package" 85 | $SUDO unzip -qq -o "$TMP_DIR/boundary.zip" -d $BIN_DIR 86 | else 87 | if [ -x "${BIN_DIR}/boundary" ] && [ "$(${BIN_DIR}/boundary version | grep "Version Number" | tr -s ' ' | cut -d' ' -f4)" = "${BOUNDARY_VERSION}" ]; then 88 | info "Boundary binary already installed in ${BIN_DIR}, skipping downloading and installing binary" 89 | else 90 | info "Downloading boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 91 | curl -o "$TMP_DIR/boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" -sfL "https://releases.hashicorp.com/boundary/${BOUNDARY_VERSION}/boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 92 | 93 | info "Downloading boundary_${BOUNDARY_VERSION}_SHA256SUMS" 94 | curl -o "$TMP_DIR/boundary_${BOUNDARY_VERSION}_SHA256SUMS" -sfL "https://releases.hashicorp.com/boundary/${BOUNDARY_VERSION}/boundary_${BOUNDARY_VERSION}_SHA256SUMS" 95 | info "Verifying downloaded boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 96 | sed -ni '/linux_'"${SUFFIX}"'.zip/p' "$TMP_DIR/boundary_${BOUNDARY_VERSION}_SHA256SUMS" 97 | sha256sum -c "$TMP_DIR/boundary_${BOUNDARY_VERSION}_SHA256SUMS" 98 | 99 | info "Unpacking boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" 100 | $SUDO unzip -qq -o "$TMP_DIR/boundary_${BOUNDARY_VERSION}_linux_${SUFFIX}.zip" -d $BIN_DIR 101 | fi 102 | fi 103 | } 104 | 105 | init_database() { 106 | $SUDO ${BIN_DIR}/boundary database init -config ${TMP_DIR}/config/boundary.hcl 107 | } 108 | 109 | setup_env 110 | setup_verify_arch 111 | install_dependencies 112 | download_and_install 113 | init_database 114 | -------------------------------------------------------------------------------- /scripts/install_consul.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | info() { 5 | echo '[INFO] ->' "$@" 6 | } 7 | 8 | fatal() { 9 | echo '[ERROR] ->' "$@" 10 | exit 1 11 | } 12 | 13 | verify_system() { 14 | if ! [ -d /run/systemd ]; then 15 | fatal 'Can not find systemd to use as a process supervisor for Consul' 16 | fi 17 | } 18 | 19 | setup_env() { 20 | SUDO=sudo 21 | if [ "$(id -u)" -eq 0 ]; then 22 | SUDO= 23 | else 24 | if [ ! -z "$SUDO_PASS" ]; then 25 | echo $SUDO_PASS | sudo -S true 26 | echo "" 27 | fi 28 | fi 29 | 30 | CONSUL_DATA_DIR=/opt/consul 31 | CONSUL_CONFIG_DIR=/etc/consul.d 32 | CONSUL_SERVICE_FILE=/etc/systemd/system/consul.service 33 | 34 | BIN_DIR=/usr/local/bin 35 | 36 | PRE_INSTALL_HASHES=$(get_installed_hashes) 37 | 38 | TMP_DIR={{.TmpDir}} 39 | SKIP_ENABLE={{.SkipEnable}} 40 | SKIP_START={{.SkipStart}} 41 | CONSUL_VERSION={{.Version}} 42 | CONSUL_ARM_SUFFIX={{.ArmSuffix}} 43 | 44 | cd $TMP_DIR 45 | } 46 | 47 | # --- set arch and suffix, fatal if architecture not supported --- 48 | setup_verify_arch() { 49 | if [ -z "$ARCH" ]; then 50 | ARCH=$(uname -m) 51 | fi 52 | case $ARCH in 53 | amd64) 54 | SUFFIX=amd64 55 | ;; 56 | x86_64) 57 | SUFFIX=amd64 58 | ;; 59 | arm64) 60 | SUFFIX=arm64 61 | ;; 62 | aarch64) 63 | SUFFIX=arm64 64 | ;; 65 | arm*) 66 | SUFFIX=${CONSUL_ARM_SUFFIX} 67 | ;; 68 | *) 69 | fatal "Unsupported architecture $ARCH" 70 | ;; 71 | esac 72 | } 73 | 74 | # --- get hashes of the current consul bin and service files 75 | get_installed_hashes() { 76 | $SUDO sha256sum ${BIN_DIR}/consul ${CONSUL_CONFIG_DIR}/* ${CONSUL_SERVICE_FILE} 2>&1 || true 77 | } 78 | 79 | has_yum() { 80 | [ -n "$(command -v yum)" ] 81 | } 82 | 83 | has_apt_get() { 84 | [ -n "$(command -v apt-get)" ] 85 | } 86 | 87 | install_dependencies() { 88 | if [ ! -x "${TMP_DIR}/consul" ]; then 89 | if ! [ -x "$(command -v unzip)" ] || ! [ -x "$(command -v curl)" ]; then 90 | if $(has_apt_get); then 91 | $SUDO apt-get install -y curl unzip 92 | elif $(has_yum); then 93 | $SUDO yum install -y curl unzip 94 | else 95 | fatal "Could not find apt-get or yum. Cannot install dependencies on this OS" 96 | exit 1 97 | fi 98 | fi 99 | fi 100 | } 101 | 102 | download_and_install() { 103 | if [ -f "${TMP_DIR}/consul.zip" ]; then 104 | info "Installing uploaded Consul package" 105 | $SUDO unzip -qq -o "$TMP_DIR/consul.zip" -d $BIN_DIR 106 | else 107 | if [ -x "${BIN_DIR}/consul" ] && [ "$(${BIN_DIR}/consul version | grep Consul | cut -d' ' -f2)" = "v${CONSUL_VERSION}" ]; then 108 | info "Consul binary already installed in ${BIN_DIR}, skipping downloading and installing binary" 109 | else 110 | info "Downloading consul_${CONSUL_VERSION}_linux_${SUFFIX}.zip" 111 | curl -o "$TMP_DIR/consul_${CONSUL_VERSION}_linux_${SUFFIX}.zip" -sfL "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_linux_${SUFFIX}.zip" 112 | 113 | info "Downloading consul_${CONSUL_VERSION}_SHA256SUMS" 114 | curl -o "$TMP_DIR/consul_${CONSUL_VERSION}_SHA256SUMS" -sfL "https://releases.hashicorp.com/consul/${CONSUL_VERSION}/consul_${CONSUL_VERSION}_SHA256SUMS" 115 | info "Verifying downloaded consul_${CONSUL_VERSION}_linux_${SUFFIX}.zip" 116 | sed -ni '/linux_'"${SUFFIX}"'.zip/p' "$TMP_DIR/consul_${CONSUL_VERSION}_SHA256SUMS" 117 | sha256sum -c "$TMP_DIR/consul_${CONSUL_VERSION}_SHA256SUMS" 118 | 119 | info "Unpacking consul_${CONSUL_VERSION}_linux_${SUFFIX}.zip" 120 | $SUDO unzip -qq -o "$TMP_DIR/consul_${CONSUL_VERSION}_linux_${SUFFIX}.zip" -d $BIN_DIR 121 | fi 122 | fi 123 | } 124 | 125 | create_user_and_config() { 126 | if $(id consul >/dev/null 2>&1); then 127 | info "User 'consul' already exists, will not create again" 128 | else 129 | info "Creating user named 'consul'" 130 | $SUDO useradd --system --home ${CONSUL_CONFIG_DIR} --shell /bin/false consul 131 | fi 132 | 133 | $SUDO mkdir --parents ${CONSUL_DATA_DIR} 134 | $SUDO mkdir --parents ${CONSUL_CONFIG_DIR}/config 135 | 136 | if [ "$(ls -A ${TMP_DIR}/config/)" ]; then 137 | info "Copying configuration files" 138 | $SUDO cp ${TMP_DIR}/config/* ${CONSUL_CONFIG_DIR} 139 | fi 140 | 141 | $SUDO chown --recursive consul:consul /opt/consul 142 | $SUDO chown --recursive consul:consul /etc/consul.d 143 | } 144 | 145 | # --- write systemd service file --- 146 | create_systemd_service_file() { 147 | info "Adding systemd service file ${CONSUL_SERVICE_FILE}" 148 | $SUDO tee ${CONSUL_SERVICE_FILE} >/dev/null </dev/null 178 | $SUDO systemctl daemon-reload >/dev/null 179 | 180 | [ "${SKIP_START}" = true ] && return 181 | 182 | POST_INSTALL_HASHES=$(get_installed_hashes) 183 | if [ "${PRE_INSTALL_HASHES}" = "${POST_INSTALL_HASHES}" ]; then 184 | info "No change detected so skipping service start" 185 | return 186 | fi 187 | 188 | info "Starting systemd service" 189 | $SUDO systemctl restart consul 190 | 191 | return 0 192 | } 193 | 194 | setup_env 195 | setup_verify_arch 196 | verify_system 197 | install_dependencies 198 | create_user_and_config 199 | download_and_install 200 | create_systemd_service_file 201 | systemd_enable_and_start 202 | -------------------------------------------------------------------------------- /scripts/install_nomad.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | info() { 5 | echo '[INFO] ->' "$@" 6 | } 7 | 8 | fatal() { 9 | echo '[ERROR] ->' "$@" 10 | exit 1 11 | } 12 | 13 | verify_system() { 14 | if ! [ -d /run/systemd ]; then 15 | fatal 'Can not find systemd to use as a process supervisor for Nomad' 16 | fi 17 | } 18 | 19 | setup_env() { 20 | SUDO=sudo 21 | if [ "$(id -u)" -eq 0 ]; then 22 | SUDO= 23 | else 24 | if [ ! -z "$SUDO_PASS" ]; then 25 | echo $SUDO_PASS | sudo -S true 26 | echo "" 27 | fi 28 | fi 29 | 30 | NOMAD_DATA_DIR=/opt/nomad 31 | NOMAD_CONFIG_DIR=/etc/nomad.d 32 | NOMAD_SERVICE_FILE=/etc/systemd/system/nomad.service 33 | 34 | BIN_DIR=/usr/local/bin 35 | 36 | PRE_INSTALL_HASHES=$(get_installed_hashes) 37 | 38 | TMP_DIR={{.TmpDir}} 39 | SKIP_ENABLE={{.SkipEnable}} 40 | SKIP_START={{.SkipStart}} 41 | NOMAD_VERSION={{.Version}} 42 | 43 | cd $TMP_DIR 44 | } 45 | 46 | # --- set arch and suffix, fatal if architecture not supported --- 47 | setup_verify_arch() { 48 | if [ -z "$ARCH" ]; then 49 | ARCH=$(uname -m) 50 | fi 51 | case $ARCH in 52 | amd64) 53 | SUFFIX=amd64 54 | ;; 55 | x86_64) 56 | SUFFIX=amd64 57 | ;; 58 | arm64) 59 | SUFFIX=arm64 60 | ;; 61 | aarch64) 62 | SUFFIX=arm64 63 | ;; 64 | arm*) 65 | SUFFIX=arm 66 | ;; 67 | *) 68 | fatal "Unsupported architecture $ARCH" 69 | ;; 70 | esac 71 | } 72 | 73 | # --- get hashes of the current nomad bin and service files 74 | get_installed_hashes() { 75 | $SUDO sha256sum ${BIN_DIR}/nomad ${NOMAD_CONFIG_DIR}/* ${NOMAD_SERVICE_FILE} 2>&1 || true 76 | } 77 | 78 | has_yum() { 79 | [ -n "$(command -v yum)" ] 80 | } 81 | 82 | has_apt_get() { 83 | [ -n "$(command -v apt-get)" ] 84 | } 85 | 86 | install_dependencies() { 87 | if [ ! -x "${TMP_DIR}/nomad" ]; then 88 | if ! [ -x "$(command -v unzip)" ] || ! [ -x "$(command -v curl)" ]; then 89 | if $(has_apt_get); then 90 | $SUDO apt-get install -y curl unzip 91 | elif $(has_yum); then 92 | $SUDO yum install -y curl unzip 93 | else 94 | fatal "Could not find apt-get or yum. Cannot install dependencies on this OS" 95 | exit 1 96 | fi 97 | fi 98 | fi 99 | } 100 | 101 | download_and_install() { 102 | if [ -f "${TMP_DIR}/nomad.zip" ]; then 103 | info "Installing uploaded Nomad package" 104 | $SUDO unzip -qq -o "$TMP_DIR/nomad.zip" -d $BIN_DIR 105 | else 106 | if [ -x "${BIN_DIR}/nomad" ] && [ "$(${BIN_DIR}/nomad version | grep Nomad | cut -d' ' -f2)" = "v${NOMAD_VERSION}" ]; then 107 | info "Nomad binary already installed in ${BIN_DIR}, skipping downloading and installing binary" 108 | else 109 | info "Downloading nomad_${NOMAD_VERSION}_linux_${SUFFIX}.zip" 110 | curl -o "$TMP_DIR/nomad_${NOMAD_VERSION}_linux_${SUFFIX}.zip" -sfL "https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_linux_${SUFFIX}.zip" 111 | 112 | info "Downloading nomad_${NOMAD_VERSION}_SHA256SUMS" 113 | curl -o "$TMP_DIR/nomad_${NOMAD_VERSION}_SHA256SUMS" -sfL "https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_SHA256SUMS" 114 | info "Verifying downloaded nomad_${NOMAD_VERSION}_linux_${SUFFIX}.zip" 115 | sed -ni '/linux_'"${SUFFIX}"'.zip/p' "$TMP_DIR/nomad_${NOMAD_VERSION}_SHA256SUMS" 116 | sha256sum -c "$TMP_DIR/nomad_${NOMAD_VERSION}_SHA256SUMS" 117 | 118 | info "Unpacking nomad_${NOMAD_VERSION}_linux_${SUFFIX}.zip" 119 | $SUDO unzip -qq -o "$TMP_DIR/nomad_${NOMAD_VERSION}_linux_${SUFFIX}.zip" -d $BIN_DIR 120 | fi 121 | fi 122 | } 123 | 124 | create_user_and_config() { 125 | $SUDO mkdir --parents ${NOMAD_DATA_DIR} 126 | $SUDO mkdir --parents ${NOMAD_CONFIG_DIR}/config 127 | 128 | if [ "$(ls -A ${TMP_DIR}/config/)" ]; then 129 | info "Copying configuration files" 130 | $SUDO cp ${TMP_DIR}/config/* ${NOMAD_CONFIG_DIR} 131 | fi 132 | } 133 | 134 | # --- write systemd service file --- 135 | create_systemd_service_file() { 136 | info "Adding systemd service file ${NOMAD_SERVICE_FILE}" 137 | $SUDO tee ${NOMAD_SERVICE_FILE} >/dev/null </dev/null 168 | $SUDO systemctl daemon-reload >/dev/null 169 | 170 | [ "${SKIP_START}" = true ] && return 171 | 172 | POST_INSTALL_HASHES=$(get_installed_hashes) 173 | if [ "${PRE_INSTALL_HASHES}" = "${POST_INSTALL_HASHES}" ]; then 174 | info "No change detected so skipping service start" 175 | return 176 | fi 177 | 178 | info "Starting systemd service" 179 | $SUDO systemctl restart nomad 180 | 181 | return 0 182 | } 183 | 184 | setup_env 185 | setup_verify_arch 186 | verify_system 187 | install_dependencies 188 | create_user_and_config 189 | download_and_install 190 | create_systemd_service_file 191 | systemd_enable_and_start -------------------------------------------------------------------------------- /scripts/install_vault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | info() { 5 | echo '[INFO] ->' "$@" 6 | } 7 | 8 | fatal() { 9 | echo '[ERROR] ->' "$@" 10 | exit 1 11 | } 12 | 13 | verify_system() { 14 | if ! [ -d /run/systemd ]; then 15 | fatal 'Can not find systemd to use as a process supervisor for Vault' 16 | fi 17 | } 18 | 19 | setup_env() { 20 | SUDO=sudo 21 | if [ "$(id -u)" -eq 0 ]; then 22 | SUDO= 23 | else 24 | if [ ! -z "$SUDO_PASS" ]; then 25 | echo $SUDO_PASS | sudo -S true 26 | echo "" 27 | fi 28 | fi 29 | 30 | VAULT_DATA_DIR=/opt/vault 31 | VAULT_CONFIG_DIR=/etc/vault.d 32 | VAULT_SERVICE_FILE=/etc/systemd/system/vault.service 33 | 34 | BIN_DIR=/usr/local/bin 35 | 36 | PRE_INSTALL_HASHES=$(get_installed_hashes) 37 | 38 | TMP_DIR={{.TmpDir}} 39 | SKIP_ENABLE={{.SkipEnable}} 40 | SKIP_START={{.SkipStart}} 41 | VAULT_VERSION={{.Version}} 42 | 43 | cd $TMP_DIR 44 | } 45 | 46 | # --- set arch and suffix, fatal if architecture not supported --- 47 | setup_verify_arch() { 48 | if [ -z "$ARCH" ]; then 49 | ARCH=$(uname -m) 50 | fi 51 | case $ARCH in 52 | amd64) 53 | SUFFIX=amd64 54 | ;; 55 | x86_64) 56 | SUFFIX=amd64 57 | ;; 58 | arm64) 59 | SUFFIX=arm64 60 | ;; 61 | aarch64) 62 | SUFFIX=arm64 63 | ;; 64 | arm*) 65 | SUFFIX=arm 66 | ;; 67 | *) 68 | fatal "Unsupported architecture $ARCH" 69 | ;; 70 | esac 71 | } 72 | 73 | # --- get hashes of the current vault bin and service files 74 | get_installed_hashes() { 75 | $SUDO sha256sum ${BIN_DIR}/vault ${VAULT_CONFIG_DIR}/* ${VAULT_SERVICE_FILE} 2>&1 || true 76 | } 77 | 78 | has_yum() { 79 | [ -n "$(command -v yum)" ] 80 | } 81 | 82 | has_apt_get() { 83 | [ -n "$(command -v apt-get)" ] 84 | } 85 | 86 | install_dependencies() { 87 | if [ ! -x "${TMP_DIR}/vault" ]; then 88 | if ! [ -x "$(command -v unzip)" ] || ! [ -x "$(command -v curl)" ]; then 89 | if $(has_apt_get); then 90 | $SUDO apt-get install -y curl unzip 91 | elif $(has_yum); then 92 | $SUDO yum install -y curl unzip 93 | else 94 | fatal "Could not find apt-get or yum. Cannot install dependencies on this OS" 95 | exit 1 96 | fi 97 | fi 98 | fi 99 | } 100 | 101 | download_and_install() { 102 | if [ -f "${TMP_DIR}/vault.zip" ]; then 103 | info "Installing uploaded Vault package" 104 | $SUDO unzip -qq -o "$TMP_DIR/vault.zip" -d $BIN_DIR 105 | $SUDO setcap cap_ipc_lock=+ep "${BIN_DIR}/vault" 106 | else 107 | if [ -x "${BIN_DIR}/vault" ] && [ "$(${BIN_DIR}/vault version | grep Vault | cut -d' ' -f2)" = "v${VAULT_VERSION}" ]; then 108 | info "Vault binary already installed in ${BIN_DIR}, skipping downloading and installing binary" 109 | else 110 | info "Downloading vault_${VAULT_VERSION}_linux_${SUFFIX}.zip" 111 | curl -o "$TMP_DIR/vault_${VAULT_VERSION}_linux_${SUFFIX}.zip" -sfL "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_${SUFFIX}.zip" 112 | 113 | info "Downloading vault_${VAULT_VERSION}_SHA256SUMS" 114 | curl -o "$TMP_DIR/vault_${VAULT_VERSION}_SHA256SUMS" -sfL "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_SHA256SUMS" 115 | info "Verifying downloaded vault_${VAULT_VERSION}_linux_${SUFFIX}.zip" 116 | sed -ni '/linux_'"${SUFFIX}"'.zip/p' "$TMP_DIR/vault_${VAULT_VERSION}_SHA256SUMS" 117 | sha256sum -c "$TMP_DIR/vault_${VAULT_VERSION}_SHA256SUMS" 118 | 119 | info "Unpacking vault_${VAULT_VERSION}_linux_${SUFFIX}.zip" 120 | $SUDO unzip -qq -o "$TMP_DIR/vault_${VAULT_VERSION}_linux_${SUFFIX}.zip" -d $BIN_DIR 121 | $SUDO setcap cap_ipc_lock=+ep "${BIN_DIR}/vault" 122 | fi 123 | fi 124 | } 125 | 126 | create_user_and_config() { 127 | if $(id vault >/dev/null 2>&1); then 128 | info "User 'vault' already exists, will not create again" 129 | else 130 | info "Creating user named 'vault'" 131 | $SUDO useradd --system --home ${VAULT_CONFIG_DIR} --shell /bin/false vault 132 | fi 133 | 134 | $SUDO mkdir --parents ${VAULT_DATA_DIR} 135 | $SUDO mkdir --parents ${VAULT_CONFIG_DIR} 136 | 137 | if [ "$(ls -A ${TMP_DIR}/config/)" ]; then 138 | info "Copying configuration files" 139 | $SUDO cp ${TMP_DIR}/config/* ${VAULT_CONFIG_DIR} 140 | fi 141 | 142 | $SUDO chown --recursive vault:vault /opt/vault 143 | $SUDO chown --recursive vault:vault /etc/vault.d 144 | } 145 | 146 | # --- write systemd service file --- 147 | create_systemd_service_file() { 148 | info "Adding systemd service file ${VAULT_SERVICE_FILE}" 149 | $SUDO tee ${VAULT_SERVICE_FILE} >/dev/null </dev/null 192 | $SUDO systemctl daemon-reload >/dev/null 193 | 194 | [ "${SKIP_START}" = true ] && return 195 | 196 | POST_INSTALL_HASHES=$(get_installed_hashes) 197 | if [ "${PRE_INSTALL_HASHES}" = "${POST_INSTALL_HASHES}" ]; then 198 | info "No change detected so skipping service start" 199 | return 200 | fi 201 | 202 | info "Starting systemd service" 203 | $SUDO systemctl restart vault 204 | 205 | return 0 206 | } 207 | 208 | cd $TMP_DIR 209 | 210 | setup_env 211 | setup_verify_arch 212 | verify_system 213 | install_dependencies 214 | create_user_and_config 215 | download_and_install 216 | create_systemd_service_file 217 | systemd_enable_and_start -------------------------------------------------------------------------------- /scripts/scripts.go: -------------------------------------------------------------------------------- 1 | package scripts 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "io" 7 | "io/fs" 8 | "text/template" 9 | ) 10 | 11 | //go:embed *.sh 12 | var content embed.FS 13 | 14 | func Open(path string) (fs.File, error) { 15 | return content.Open(path) 16 | } 17 | 18 | func RenderScript(name string, data interface{}) (io.Reader, error) { 19 | var buf bytes.Buffer 20 | 21 | t, err := template.ParseFS(content, name) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | if err := t.Execute(&buf, data); err != nil { 27 | return nil, err 28 | } 29 | 30 | return &buf, nil 31 | } 32 | -------------------------------------------------------------------------------- /scripts/service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | info() { 5 | echo '[INFO] ->' "$@" 6 | } 7 | 8 | fatal() { 9 | echo '[ERROR] ->' "$@" 10 | exit 1 11 | } 12 | 13 | verify_system() { 14 | if ! [ -d /run/systemd ]; then 15 | fatal "Can not find systemd to use as a process supervisor" 16 | fi 17 | } 18 | 19 | setup_env() { 20 | SUDO=sudo 21 | if [ "$(id -u)" -eq 0 ]; then 22 | SUDO= 23 | else 24 | if [ ! -z "$SUDO_PASS" ]; then 25 | echo $SUDO_PASS | sudo -S true 26 | echo "" 27 | fi 28 | fi 29 | } 30 | 31 | execute() { 32 | $SUDO systemctl $ACTION $SERVICE 33 | } 34 | 35 | verify_system 36 | setup_env 37 | execute -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | info() { 5 | echo '[INFO] ->' "$@" 6 | } 7 | 8 | fatal() { 9 | echo '[ERROR] ->' "$@" 10 | exit 1 11 | } 12 | 13 | verify_system() { 14 | if ! [ -d /run/systemd ]; then 15 | fatal "Can not find systemd to use as a process supervisor" 16 | fi 17 | } 18 | 19 | setup_env() { 20 | SUDO=sudo 21 | if [ "$(id -u)" -eq 0 ]; then 22 | SUDO= 23 | else 24 | if [ ! -z "$SUDO_PASS" ]; then 25 | echo $SUDO_PASS | sudo -S true 26 | echo "" 27 | fi 28 | fi 29 | 30 | DATA_DIR="/opt/$SERVICE" 31 | CONFIG_DIR="/etc/$SERVICE.d" 32 | SERVICE_FILE="/etc/systemd/system/$SERVICE.service" 33 | BIN_DIR=/usr/local/bin 34 | } 35 | 36 | stop_and_disable_service() { 37 | info "Stopping and disabling systemd service" 38 | $SUDO systemctl stop $SERVICE 39 | $SUDO systemctl disable $SERVICE 40 | $SUDO systemctl daemon-reload 41 | } 42 | 43 | clean_up() { 44 | info "Removing installation" 45 | $SUDO rm -rf $CONFIG_DIR 46 | $SUDO rm -rf $DATA_DIR 47 | $SUDO rm -rf $SERVICE_FILE 48 | $SUDO rm -rf $BIN_DIR/$SERVICE 49 | } 50 | 51 | verify_system 52 | setup_env 53 | stop_and_disable_service 54 | clean_up --------------------------------------------------------------------------------